dongjoon-hyun commented on code in PR #58965:
URL: https://github.com/apache/spark/pull/58965#discussion_r4429216332


##########
core/src/main/scala/org/apache/spark/status/AppHistoryServerPlugin.scala:
##########
@@ -36,8 +41,26 @@ private[spark] trait AppHistoryServerPlugin {
    */
   def setupUI(ui: SparkUI): Unit
 
+  /**
+   * Registers static resource handlers (JS/CSS bundled with the plugin) 
directly on the
+   * History Server's own UI, rather than on a per-application SparkUI. 
Invoked exactly once,
+   * when the History Server starts up.
+   *
+   * A handler attached via [[setupUI]] lives on that one application's 
SparkUI and is torn

Review Comment:
   **[4] This reads as if registering here replaces the handler attached in 
`setupUI`, but `SQLTab` still needs its own.**
   
   The live driver UI never calls this hook, so SQLTab.scala:37 must keep 
registering `/static/sql` for it. A later cleanup that follows "Registering it 
here instead" and deletes that line would leave the path unserved on the live 
UI of every application that never creates a `StreamingQueryManager`: the only 
other registration, StreamingQueryTab.scala:35, is built only from the lazy 
`SharedState.streamingQueryStatusListener` (SharedState.scala:135-145). No test 
would catch it. In the History Server, on the other hand, those per-application 
copies no longer serve anything, because the handler registered here is 
attached first and answers every `/static/sql` request.
   
   Could the doc add a sentence such as "Tabs still register the same handler 
on each SparkUI because the live driver UI never calls this hook; in the 
History Server, the handler registered here keeps the path served after those 
per-application copies are detached"? Alternatively, `SQLTab` and 
`StreamingQueryTab` could register theirs only when `sparkUI.sc.isDefined`, the 
way ExecutorsTab.scala:33-35 gates live-only features; 
FsHistoryProvider.scala:445 is the only `SparkUI.create(None, ...)` in main 
code.



##########
core/src/main/scala/org/apache/spark/deploy/history/HistoryServer.scala:
##########
@@ -155,6 +156,7 @@ class HistoryServer(
     attachHandler(ApiRootResource.getServletHandler(this))
 
     addStaticHandler(SparkUI.STATIC_RESOURCE_DIR)
+    AppHistoryServerPlugin.loadPlugins().foreach(_.setupStaticResources(this))

Review Comment:
   **[1] A plugin that fails to load now stops the History Server from 
starting.**
   
   This line runs inside the constructor (`initialize()` at L144), before 
`bind()` in `main` (L320-321). If a jar on the classpath lists an 
`AppHistoryServerPlugin` provider whose class is missing or cannot be linked, 
or whose constructor throws, the `ServiceLoader` iterator throws 
`ServiceConfigurationError` (or a raw `LinkageError` from `hasNext`) out of 
`new HistoryServer(...)`, and the daemon exits. Before this PR, only 
`FsHistoryProvider.getAppUI` and `rebuildAppStore` loaded plugins 
(FsHistoryProvider.scala:450 and 1436), so the same plugin broke only the 
application UIs, while the listing page, `/api/v1/applications` and event log 
downloads kept working.
   
   A `NonFatal` guard around `setupStaticResources` alone would not be enough, 
because these errors come from the iterator, and `NonFatal` does not match 
`LinkageError`. Could this call skip a failing plugin with a warning, catching 
`Throwable` around both `hasNext` and `next()` as 
`HadoopDelegationTokenManager.loadProviders` does 
(HadoopDelegationTokenManager.scala:339-357)? If failing fast is intended, 
could the PR description say so instead?



##########
core/src/main/scala/org/apache/spark/deploy/history/FsHistoryProvider.scala:
##########
@@ -1713,9 +1713,7 @@ private[history] class FsHistoryProvider(conf: SparkConf, 
clock: Clock)
     store
   }
 
-  private def loadPlugins(): Iterable[AppHistoryServerPlugin] = {
-    ServiceLoader.load(classOf[AppHistoryServerPlugin], 
Utils.getContextOrSparkClassLoader).asScala
-  }
+  private def loadPlugins(): Iterable[AppHistoryServerPlugin] = 
AppHistoryServerPlugin.loadPlugins()

Review Comment:
   **[7] Nit: this is now only an alias of 
`AppHistoryServerPlugin.loadPlugins()`.**
   
   Could L450 and L1436 call `AppHistoryServerPlugin.loadPlugins()` directly, 
as HistoryServer.scala:159 does, and this method go? `AppHistoryServerPlugin` 
already resolves through the `org.apache.spark.status._` import (L51), and L450 
stays within 100 characters. With one name for the loader, a later change such 
as caching cannot land on only one of the two call paths.



##########
core/src/main/scala/org/apache/spark/deploy/history/HistoryServer.scala:
##########
@@ -155,6 +156,7 @@ class HistoryServer(
     attachHandler(ApiRootResource.getServletHandler(this))
 
     addStaticHandler(SparkUI.STATIC_RESOURCE_DIR)
+    AppHistoryServerPlugin.loadPlugins().foreach(_.setupStaticResources(this))

Review Comment:
   **[2] No test covers the new handler, and the description does not say why.**
   
   The HistoryServer suites in core cannot exercise this line: core's test 
classpath has no `AppHistoryServerPlugin` services file, so 
`SQLHistoryServerPlugin` is never loaded there. Removing this line or the 
override in SQLHistoryServerPlugin.scala:37-39, or changing the mount path, 
would bring the 404s back without failing any test.
   
   sql/core already has test code in `org.apache.spark.deploy.history` 
(`Utils.scala`), which reaches the `private[history]` classes, and it depends 
on core's test-jar. Could you add a small suite there that starts a 
`HistoryServer` over an empty log directory on port 0, as 
HistoryServerSuite.scala:480 does, and checks that `GET 
/static/sql/spark-sql-viz.js` returns 200 before any application is loaded, 
e.g. with `HistoryServerSuite.getContentAndCode(url)`? Without this line, the 
request falls through to the core `/static` handler, whose directory has no 
`sql/` subdirectory, and returns 404.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/ui/SQLHistoryServerPlugin.scala:
##########
@@ -34,6 +34,10 @@ class SQLHistoryServerPlugin extends AppHistoryServerPlugin {
     }
   }
 
+  override def setupStaticResources(ui: WebUI): Unit = {
+      ui.addStaticHandler(SQLTab.STATIC_RESOURCE_DIR, "/static/sql")

Review Comment:
   **[8] Nit: this line is indented by 6 spaces.**
   
   The other method bodies in this class use 4 (L27 and L31-34). Could you 
indent it by 4?



##########
core/src/main/scala/org/apache/spark/status/AppHistoryServerPlugin.scala:
##########
@@ -36,8 +41,26 @@ private[spark] trait AppHistoryServerPlugin {
    */
   def setupUI(ui: SparkUI): Unit
 
+  /**
+   * Registers static resource handlers (JS/CSS bundled with the plugin) 
directly on the
+   * History Server's own UI, rather than on a per-application SparkUI. 
Invoked exactly once,
+   * when the History Server starts up.
+   *
+   * A handler attached via [[setupUI]] lives on that one application's 
SparkUI and is torn
+   * down whenever the application cache evicts it, even though the underlying 
Jetty context
+   * path and resources are shared by every application on that server. 
Registering it here
+   * instead ties its lifetime to the History Server process itself.
+   */
+  def setupStaticResources(ui: WebUI): Unit = {}

Review Comment:
   **[5] Could the hook declare its static resources instead of receiving the 
whole `HistoryServer`?**
   
   `setupStaticResources(ui: WebUI)` hands every plugin the History Server 
itself, before `bind()`, with `attachPage`, `attachTab`, `attachHandler`, 
`detachHandler(path)`, and even `bind()` and `stop()`, although the only 
override adds one static handler. The trait is `private[spark]` and plugins are 
trusted code, so this is about the contract rather than a bug: from inside the 
constructor, a plugin could detach the server's own `/static` or `/api` 
handler, or attach pages to its root, which is more than the name and doc 
promise. A declarative member in the style of `displayOrder` would do the same 
job:
   
   ```scala
     def staticResources: Seq[(String, String)] = Nil
   ```
   
   with HistoryServer.scala:159 as
   
   ```scala
       AppHistoryServerPlugin.loadPlugins().flatMap(_.staticResources).foreach {
         case (dir, path) => addStaticHandler(dir, path)
       }
   ```
   
   and in `SQLHistoryServerPlugin`
   
   ```scala
     override def staticResources: Seq[(String, String)] =
       Seq(SQLTab.STATIC_RESOURCE_DIR -> "/static/sql")
   ```
   
   The handler is then added at the same point and in the same order, the 
server keeps control of its own handlers, and "Invoked exactly once" (L46) is 
no longer needed.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/ui/SQLHistoryServerPlugin.scala:
##########
@@ -34,6 +34,10 @@ class SQLHistoryServerPlugin extends AppHistoryServerPlugin {
     }
   }
 
+  override def setupStaticResources(ui: WebUI): Unit = {
+      ui.addStaticHandler(SQLTab.STATIC_RESOURCE_DIR, "/static/sql")

Review Comment:
   **[6] Nit: this types the `/static/sql` mount path once more.**
   
   SQLTab.scala:37 has the same literal, and StreamingQueryTab.scala:35 has a 
third copy. `StreamingQueryTab` also keeps its own copy of the directory 
(StreamingQueryTab.scala:41), since the `private[ui]` constant at 
SQLTab.scala:43 is not visible from `org.apache.spark.sql.streaming.ui`. If the 
path later changes in `SQLTab` and in the pages that link to it 
(ExecutionPage.scala, AllExecutionsPage.scala) but not here, the 404s come back 
only on servers with no application loaded and after evictions, which is easy 
to miss. Could object `SQLTab` own the mount in one place, e.g. `private[sql] 
def addStaticHandler(ui: WebUI): Unit = 
ui.addStaticHandler(STATIC_RESOURCE_DIR, "/static/sql")`, used by 
SQLTab.scala:37, this override and StreamingQueryTab.scala:35 (or, with [5], 
one shared `(dir, path)` pair)? `STATIC_RESOURCE_DIR` could then stay private.



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