-
Notifications
You must be signed in to change notification settings - Fork 29.3k
[SPARK-4411] [Web UI] Add "kill" link for jobs in the UI #15441
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 10 commits
af461cc
7f52874
7a6143a
584240a
25fc0fd
ba16839
a0eee0c
d0e2083
f2519fc
999f83a
c9b2454
b1e77ba
0162c81
12bc267
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -218,7 +218,8 @@ private[ui] class AllJobsPage(parent: JobsTab) extends WebUIPage("") { | |
| request: HttpServletRequest, | ||
| tableHeaderId: String, | ||
| jobTag: String, | ||
| jobs: Seq[JobUIData]): Seq[Node] = { | ||
| jobs: Seq[JobUIData], | ||
| killEnabled: Boolean): Seq[Node] = { | ||
| val allParameters = request.getParameterMap.asScala.toMap | ||
| val parameterOtherTable = allParameters.filterNot(_._1.startsWith(jobTag)) | ||
| .map(para => para._1 + "=" + para._2(0)) | ||
|
|
@@ -264,6 +265,7 @@ private[ui] class AllJobsPage(parent: JobsTab) extends WebUIPage("") { | |
| parameterOtherTable, | ||
| parent.jobProgresslistener.stageIdToInfo, | ||
| parent.jobProgresslistener.stageIdToData, | ||
| killEnabled, | ||
| currentTime, | ||
| jobIdTitle, | ||
| pageSize = jobPageSize, | ||
|
|
@@ -290,9 +292,12 @@ private[ui] class AllJobsPage(parent: JobsTab) extends WebUIPage("") { | |
| val completedJobs = listener.completedJobs.reverse.toSeq | ||
| val failedJobs = listener.failedJobs.reverse.toSeq | ||
|
|
||
| val activeJobsTable = jobsTable(request, "active", "activeJob", activeJobs) | ||
| val completedJobsTable = jobsTable(request, "completed", "completedJob", completedJobs) | ||
| val failedJobsTable = jobsTable(request, "failed", "failedJob", failedJobs) | ||
| val activeJobsTable = | ||
| jobsTable(request, "active", "activeJob", activeJobs, killEnabled = parent.killEnabled) | ||
| val completedJobsTable = | ||
| jobsTable(request, "completed", "completedJob", completedJobs, killEnabled = false) | ||
| val failedJobsTable = | ||
| jobsTable(request, "failed", "failedJob", failedJobs, killEnabled = false) | ||
|
|
||
| val shouldShowActiveJobs = activeJobs.nonEmpty | ||
| val shouldShowCompletedJobs = completedJobs.nonEmpty | ||
|
|
@@ -483,6 +488,7 @@ private[ui] class JobPagedTable( | |
| parameterOtherTable: Iterable[String], | ||
| stageIdToInfo: HashMap[Int, StageInfo], | ||
| stageIdToData: HashMap[(Int, Int), StageUIData], | ||
| killEnabled: Boolean, | ||
| currentTime: Long, | ||
| jobIdTitle: String, | ||
| pageSize: Int, | ||
|
|
@@ -586,12 +592,29 @@ private[ui] class JobPagedTable( | |
| override def row(jobTableRow: JobTableRowData): Seq[Node] = { | ||
| val job = jobTableRow.jobData | ||
|
|
||
| val killLink = if (killEnabled) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm, does this work? there's no 'else' so this becomes an
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice catch, since it's supposed to be html I'll add a |
||
| val confirm = | ||
| s"if (window.confirm('Are you sure you want to kill job ${job.jobId} ?')) " + | ||
| "{ this.parentNode.submit(); return true; } else { return false; }" | ||
| // SPARK-6846 this should be POST-only but YARN AM won't proxy POST | ||
| /* | ||
| val killLinkUri = s"$basePathUri/jobs/job/kill/" | ||
| <form action={killLinkUri} method="POST" style="display:inline"> | ||
| <input type="hidden" name="id" value={job.jobId.toString}/> | ||
| <input type="hidden" name="terminate" value="true"/> | ||
| <a href="#" onclick={confirm} class="kill-link">(kill)</a> | ||
| </form> | ||
| */ | ||
| val killLinkUri = s"$basePath/jobs/job/kill/?id=${job.jobId}&terminate=true" | ||
| <a href={killLinkUri} onclick={confirm} class="kill-link">(kill)</a> | ||
| } | ||
|
|
||
| <tr id={"job-" + job.jobId}> | ||
| <td> | ||
| {job.jobId} {job.jobGroup.map(id => s"($id)").getOrElse("")} | ||
| </td> | ||
| <td> | ||
| {jobTableRow.jobDescription} | ||
| {jobTableRow.jobDescription} {killLink} | ||
| <a href={jobTableRow.detailUrl} class="name-link">{jobTableRow.lastStageName}</a> | ||
| </td> | ||
| <td> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,8 @@ | |
|
|
||
| package org.apache.spark.ui.jobs | ||
|
|
||
| import javax.servlet.http.HttpServletRequest | ||
|
|
||
| import org.apache.spark.scheduler.SchedulingMode | ||
| import org.apache.spark.ui.{SparkUI, SparkUITab} | ||
|
|
||
|
|
@@ -35,4 +37,18 @@ private[ui] class JobsTab(parent: SparkUI) extends SparkUITab(parent, "jobs") { | |
|
|
||
| attachPage(new AllJobsPage(this)) | ||
| attachPage(new JobPage(this)) | ||
|
|
||
| def handleKillRequest(request: HttpServletRequest): Unit = { | ||
| if (killEnabled && (parent.securityManager.checkModifyPermissions(request.getRemoteUser))) { | ||
| val killFlag = Option(request.getParameter("terminate")).getOrElse("false").toBoolean | ||
| val jobId = Option(request.getParameter("id")).getOrElse("-1").toInt | ||
| if (jobId >= 0 && killFlag && jobProgresslistener.activeJobs.contains(jobId)) { | ||
| sc.get.cancelJob(jobId) | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Creating an val jobId = Option(request.getParameter("id"))
jobId.foreach { id =>
if (killFlag && jobProgresslistener.activeJobs.contains(id)) {
sc.get.cancelJob(id)
}
}
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similarly, there is no need for sc.foreach(_.cancelJob(id))
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And if the Job isn't actually going to be canceled, then there is no need to delay the page refresh (which I'm not entirely happy with, but I'm not going to try to resolve that issue right now.) So... sc.foreach { sparkContext =>
sparkContext.cancelJob(id)
Thread.sleep(100)
}And we better make that val jobId = Option(request.getParameter("id")).map(_.toInt)
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I copied this code from |
||
| // Do a quick pause here to give Spark time to kill the job so it shows up as | ||
| // killed after the refresh. Note that this will block the serving thread so the | ||
| // time should be limited in duration. | ||
| Thread.sleep(100) | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -194,6 +194,22 @@ class UISeleniumSuite extends SparkFunSuite with WebBrowser with Matchers with B | |
| sc.parallelize(1 to 10).map{x => Thread.sleep(10000); x}.countAsync() | ||
| } | ||
|
|
||
| withSpark(newSparkContext(killEnabled = true)) { sc => | ||
| runSlowJob(sc) | ||
| eventually(timeout(5 seconds), interval(50 milliseconds)) { | ||
| goToUi(sc, "/jobs") | ||
| assert(hasKillLink) | ||
| } | ||
| } | ||
|
|
||
| withSpark(newSparkContext(killEnabled = false)) { sc => | ||
| runSlowJob(sc) | ||
| eventually(timeout(5 seconds), interval(50 milliseconds)) { | ||
| goToUi(sc, "/jobs") | ||
| assert(!hasKillLink) | ||
| } | ||
| } | ||
|
|
||
| withSpark(newSparkContext(killEnabled = true)) { sc => | ||
| runSlowJob(sc) | ||
| eventually(timeout(5 seconds), interval(50 milliseconds)) { | ||
|
|
@@ -453,20 +469,24 @@ class UISeleniumSuite extends SparkFunSuite with WebBrowser with Matchers with B | |
| } | ||
|
|
||
| test("kill stage POST/GET response is correct") { | ||
| def getResponseCode(url: URL, method: String): Int = { | ||
| val connection = url.openConnection().asInstanceOf[HttpURLConnection] | ||
| connection.setRequestMethod(method) | ||
| connection.connect() | ||
| val code = connection.getResponseCode() | ||
| connection.disconnect() | ||
| code | ||
| withSpark(newSparkContext(killEnabled = true)) { sc => | ||
| sc.parallelize(1 to 10).map{x => Thread.sleep(10000); x}.countAsync() | ||
| eventually(timeout(5 seconds), interval(50 milliseconds)) { | ||
| val url = new URL( | ||
| sc.ui.get.appUIAddress.stripSuffix("/") + "/stages/stage/kill/?id=0&terminate=true") | ||
| // SPARK-6846: should be POST only but YARN AM doesn't proxy POST | ||
| getResponseCode(url, "GET") should be (200) | ||
| getResponseCode(url, "POST") should be (200) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| test("kill job POST/GET response is correct") { | ||
| withSpark(newSparkContext(killEnabled = true)) { sc => | ||
| sc.parallelize(1 to 10).map{x => Thread.sleep(10000); x}.countAsync() | ||
| eventually(timeout(5 seconds), interval(50 milliseconds)) { | ||
| val url = new URL( | ||
| sc.ui.get.appUIAddress.stripSuffix("/") + "/stages/stage/kill/?id=0&terminate=true") | ||
| sc.ui.get.appUIAddress.stripSuffix("/") + "/jobs/job/kill/?id=0&terminate=true") | ||
| // SPARK-6846: should be POST only but YARN AM doesn't proxy POST | ||
| getResponseCode(url, "GET") should be (200) | ||
| getResponseCode(url, "POST") should be (200) | ||
|
|
@@ -651,6 +671,15 @@ class UISeleniumSuite extends SparkFunSuite with WebBrowser with Matchers with B | |
| } | ||
| } | ||
|
|
||
| def getResponseCode(url: URL, method: String): Int = { | ||
| val connection = url.openConnection().asInstanceOf[HttpURLConnection] | ||
| connection.setRequestMethod(method) | ||
| connection.connect() | ||
| val code = connection.getResponseCode() | ||
| connection.disconnect() | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For total tidiness you might use try-finally here to disconnect on all paths.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might just because it's late and I'm tired but I'm not quite sure where you think the try-finally should be
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I just mean
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, I didn't know scala could do that (how it returns), still learning new things every day |
||
| code | ||
| } | ||
|
|
||
| def goToUi(sc: SparkContext, path: String): Unit = { | ||
| goToUi(sc.ui.get, path) | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -624,7 +624,7 @@ Apart from these, the following properties are also available, and may be useful | |
| <td><code>spark.ui.killEnabled</code></td> | ||
| <td>true</td> | ||
| <td> | ||
| Allows stages and corresponding jobs to be killed from the web ui. | ||
| Allows jobs and stages to be killed from the web ui. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: web ui -> web UI |
||
| </td> | ||
| </tr> | ||
| <tr> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See #15603 which reminds me that these two tabs don't need to be members. They can be locals. You can make the change for both here.