Skip to content

Commit

Permalink
Make BSP requests robust to some target failures
Browse files Browse the repository at this point in the history
The request of the form buildTarget/* often take a sequence of build
targets as parameter. So far if there is an error on a single build
target, the entire request fails.
This is not the best because the client wants the result of the other
build targets anyway:
For example:
- workspace/buildTargets: if one build target has an invalid Scala
version we still want to import the other ones
- buildTarget/scalacOptions: if a dependency cannot be resolved we still
want to import the build targets that do not depend on it
- buildTarget/scalaMainClasses: if buildTarget does not compile we still
want the main classes of the other targets
...

The change is to respond to BSP requests with the successful
build targets and  to ignore the failed ones.
This is implemented the same in Bloop since before BSP in sbt.

In  build-server-protocol/build-server-protocol#204,
I made a proposal to also add the failed build targets in the response.
  • Loading branch information
adpi2 authored and Amina Adewusi committed Oct 30, 2021
1 parent 9a81175 commit 60cc912
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 34 deletions.
71 changes: 48 additions & 23 deletions main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala
Original file line number Diff line number Diff line change
Expand Up @@ -90,16 +90,16 @@ object BuildServerProtocol {
val workspace = Keys.bspFullWorkspace.value
val state = Keys.state.value
val allTargets = ScopeFilter.in(workspace.scopes.values.toSeq)
val sbtTargets: List[Def.Initialize[Task[BuildTarget]]] = workspace.builds.map {
val sbtTargets = workspace.builds.map {
case (buildTargetIdentifier, loadedBuildUnit) =>
val buildFor = workspace.buildToScope.getOrElse(buildTargetIdentifier, Nil)
sbtBuildTarget(loadedBuildUnit, buildTargetIdentifier, buildFor)
sbtBuildTarget(loadedBuildUnit, buildTargetIdentifier, buildFor).result
}.toList
Def.task {
val buildTargets = Keys.bspBuildTarget.all(allTargets).value.toVector
val allBuildTargets = buildTargets ++ sbtTargets.join.value
state.respondEvent(WorkspaceBuildTargetsResult(allBuildTargets))
allBuildTargets
val buildTargets = Keys.bspBuildTarget.result.all(allTargets).value
val successfulBuildTargets = anyOrThrow(buildTargets ++ sbtTargets.join.value)
state.respondEvent(WorkspaceBuildTargetsResult(successfulBuildTargets.toVector))
successfulBuildTargets
}
}.value,
// https://github.com/build-server-protocol/build-server-protocol/blob/master/docs/specification.md#build-target-sources-request
Expand All @@ -110,8 +110,8 @@ object BuildServerProtocol {
val filter = ScopeFilter.in(workspace.scopes.values.toList)
// run the worker task concurrently
Def.task {
val items = bspBuildTargetSourcesItem.all(filter).value
val buildItems = workspace.builds.toVector.map {
val items = bspBuildTargetSourcesItem.result.all(filter).value
val buildItems = workspace.builds.map {
case (id, loadedBuildUnit) =>
val base = loadedBuildUnit.localBase
val sbtFiles = configurationSources(base)
Expand All @@ -130,9 +130,10 @@ object BuildServerProtocol {
add(pluginData.managedSourceDirectories, SourceItemKind.Directory, generated = true)
add(pluginData.managedSources, SourceItemKind.File, generated = true)
add(sbtFiles, SourceItemKind.File, generated = false)
SourcesItem(id, all.result())
Value(SourcesItem(id, all.result()))
}
val result = SourcesResult((items ++ buildItems).toVector)
val successfulItems = anyOrThrow(items ++ buildItems)
val result = SourcesResult(successfulItems.toVector)
s.respondEvent(result)
}
}.evaluated,
Expand All @@ -145,8 +146,9 @@ object BuildServerProtocol {
val filter = ScopeFilter.in(workspace.scopes.values.toList)
// run the worker task concurrently
Def.task {
val items = bspBuildTargetResourcesItem.all(filter).value
val result = ResourcesResult(items.toVector)
val items = bspBuildTargetResourcesItem.result.all(filter).value
val successfulItems = anyOrThrow(items)
val result = ResourcesResult(successfulItems.toVector)
s.respondEvent(result)
}
}.evaluated,
Expand All @@ -159,8 +161,9 @@ object BuildServerProtocol {
// run the worker task concurrently
Def.task {
import sbt.internal.bsp.codec.JsonProtocol._
val items = bspBuildTargetDependencySourcesItem.all(filter).value
val result = DependencySourcesResult(items.toVector)
val items = bspBuildTargetDependencySourcesItem.result.all(filter).value
val successfulItems = anyOrThrow(items)
val result = DependencySourcesResult(successfulItems.toVector)
s.respondEvent(result)
}
}.evaluated,
Expand All @@ -172,8 +175,12 @@ object BuildServerProtocol {
workspace.warnIfBuildsNonEmpty(Method.Compile, s.log)
val filter = ScopeFilter.in(workspace.scopes.values.toList)
Def.task {
val statusCode = Keys.bspBuildTargetCompileItem.all(filter).value.max
s.respondEvent(BspCompileResult(None, statusCode))
val statusCodes = Keys.bspBuildTargetCompileItem.result.all(filter).value
val aggregatedStatusCode = allOrThrow(statusCodes) match {
case Seq() => StatusCode.Success
case codes => codes.max
}
s.respondEvent(BspCompileResult(None, aggregatedStatusCode))
}
}.evaluated,
bspBuildTargetCompile / aggregate := false,
Expand All @@ -188,7 +195,7 @@ object BuildServerProtocol {

val filter = ScopeFilter.in(workspace.scopes.values.toList)
Def.task {
val items = bspBuildTargetScalacOptionsItem.all(filter).value
val items = bspBuildTargetScalacOptionsItem.result.all(filter).value
val appProvider = appConfiguration.value.provider()
val sbtJars = appProvider.mainClasspath()
val buildItems = builds.map {
Expand All @@ -197,14 +204,16 @@ object BuildServerProtocol {
val scalacOptions = plugins.pluginData.scalacOptions
val pluginClassPath = plugins.classpath
val classpath = (pluginClassPath ++ sbtJars).map(_.toURI).toVector
ScalacOptionsItem(
val item = ScalacOptionsItem(
build._1,
scalacOptions.toVector,
classpath,
new File(build._2.localBase, "project/target").toURI
)
Value(item)
}
val result = ScalacOptionsResult((items ++ buildItems).toVector)
val successfulItems = anyOrThrow(items ++ buildItems)
val result = ScalacOptionsResult(successfulItems.toVector)
s.respondEvent(result)
}
}.evaluated,
Expand All @@ -216,8 +225,9 @@ object BuildServerProtocol {
workspace.warnIfBuildsNonEmpty(Method.ScalaTestClasses, s.log)
val filter = ScopeFilter.in(workspace.scopes.values.toList)
Def.task {
val items = bspScalaTestClassesItem.all(filter).value
val result = ScalaTestClassesResult(items.toVector, None)
val items = bspScalaTestClassesItem.result.all(filter).value
val successfulItems = anyOrThrow(items)
val result = ScalaTestClassesResult(successfulItems.toVector, None)
s.respondEvent(result)
}
}.evaluated,
Expand All @@ -228,8 +238,9 @@ object BuildServerProtocol {
workspace.warnIfBuildsNonEmpty(Method.ScalaMainClasses, s.log)
val filter = ScopeFilter.in(workspace.scopes.values.toList)
Def.task {
val items = bspScalaMainClassesItem.all(filter).value
val result = ScalaMainClassesResult(items.toVector, None)
val items = bspScalaMainClassesItem.result.all(filter).value
val successfulItems = anyOrThrow(items)
val result = ScalaMainClassesResult(successfulItems.toVector, None)
s.respondEvent(result)
}
}.evaluated,
Expand Down Expand Up @@ -846,6 +857,20 @@ object BuildServerProtocol {
case _ => sys.error(s"unexpected $ref")
}

private def anyOrThrow[T](results: Seq[Result[T]]): Seq[T] = {
val successes = results.collect { case Value(v) => v }
val errors = results.collect { case Inc(cause) => cause }
if (successes.nonEmpty || errors.isEmpty) successes
else throw Incomplete(None, causes = errors)
}

private def allOrThrow[T](results: Seq[Result[T]]): Seq[T] = {
val successes = results.collect { case Value(v) => v }
val errors = results.collect { case Inc(cause) => cause }
if (errors.isEmpty) successes
else throw Incomplete(None, causes = errors)
}

private case class SemanticVersion(major: Int, minor: Int) extends Ordered[SemanticVersion] {
override def compare(that: SemanticVersion): Int = {
if (that.major != major) major.compare(that.major)
Expand Down
15 changes: 15 additions & 0 deletions server-test/src/server-test/buildserver/build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,18 @@ lazy val respondError = project.in(file("respond-error"))
)

lazy val util = project

def somethingBad = throw new MessageOnlyException("I am a bad build target")
// other build targets should not be affected by this bad build target
lazy val badBuildTarget = project.in(file("bad-build-target"))
.settings(
Compile / bspBuildTarget := somethingBad,
Compile / bspBuildTargetSourcesItem := somethingBad,
Compile / bspBuildTargetResourcesItem := somethingBad,
Compile / bspBuildTargetDependencySourcesItem := somethingBad,
Compile / bspBuildTargetScalacOptionsItem := somethingBad,
Compile / bspBuildTargetCompileItem := somethingBad,
Compile / bspScalaMainClasses := somethingBad,
Test / bspBuildTarget := somethingBad,
Test / bspScalaTestClasses := somethingBad,
)
25 changes: 14 additions & 11 deletions server-test/src/test/scala/testpkg/BuildServerTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@

package testpkg

import sbt.internal.bsp.SourcesResult
import sbt.internal.bsp.WorkspaceBuildTargetsResult
import sbt.internal.bsp.{ BspCompileResult, SourcesResult, StatusCode, WorkspaceBuildTargetsResult }
import sbt.internal.langserver.ErrorCodes
import sbt.IO

Expand Down Expand Up @@ -41,13 +40,15 @@ object BuildServerTest extends AbstractServerTest {
val buildServerBuildTarget =
result.targets.find(_.displayName.contains("buildserver-build")).get
assert(buildServerBuildTarget.id.uri.toString.endsWith("#buildserver-build"))
assert(!result.targets.exists(_.displayName.contains("badBuildTarget")))
}

test("buildTarget/sources") { _ =>
val buildTarget = buildTargetUri("util", "Compile")
val badBuildTarget = buildTargetUri("badBuildTarget", "Compile")
svr.sendJsonRpc(
s"""{ "jsonrpc": "2.0", "id": "24", "method": "buildTarget/sources", "params": {
| "targets": [{ "uri": "$buildTarget" }]
| "targets": [{ "uri": "$buildTarget" }, { "uri": "$badBuildTarget" }]
|} }""".stripMargin
)
assert(processing("buildTarget/sources"))
Expand Down Expand Up @@ -88,17 +89,16 @@ object BuildServerTest extends AbstractServerTest {
|} }""".stripMargin
)
assert(processing("buildTarget/compile"))
assert(svr.waitForString(10.seconds) { s =>
(s contains """"id":"32"""") &&
(s contains """"statusCode":1""")
})
val res = svr.waitFor[BspCompileResult](10.seconds)
assert(res.statusCode == StatusCode.Success)
}

test("buildTarget/scalacOptions") { _ =>
val buildTarget = buildTargetUri("util", "Compile")
val badBuildTarget = buildTargetUri("badBuildTarget", "Compile")
svr.sendJsonRpc(
s"""{ "jsonrpc": "2.0", "id": "40", "method": "buildTarget/scalacOptions", "params": {
| "targets": [{ "uri": "$buildTarget" }]
| "targets": [{ "uri": "$buildTarget" }, { "uri": "$badBuildTarget" }]
|} }""".stripMargin
)
assert(processing("buildTarget/scalacOptions"))
Expand Down Expand Up @@ -176,9 +176,10 @@ object BuildServerTest extends AbstractServerTest {

test("buildTarget/scalaMainClasses") { _ =>
val buildTarget = buildTargetUri("runAndTest", "Compile")
val badBuildTarget = buildTargetUri("badBuildTarget", "Compile")
svr.sendJsonRpc(
s"""{ "jsonrpc": "2.0", "id": "56", "method": "buildTarget/scalaMainClasses", "params": {
| "targets": [{ "uri": "$buildTarget" }]
| "targets": [{ "uri": "$buildTarget" }, { "uri": "$badBuildTarget" }]
|} }""".stripMargin
)
assert(processing("buildTarget/scalaMainClasses"))
Expand Down Expand Up @@ -210,9 +211,10 @@ object BuildServerTest extends AbstractServerTest {

test("buildTarget/scalaTestClasses") { _ =>
val buildTarget = buildTargetUri("runAndTest", "Test")
val badBuildTarget = buildTargetUri("badBuildTarget", "Test")
svr.sendJsonRpc(
s"""{ "jsonrpc": "2.0", "id": "72", "method": "buildTarget/scalaTestClasses", "params": {
| "targets": [{ "uri": "$buildTarget" }]
| "targets": [{ "uri": "$buildTarget" }, { "uri": "$badBuildTarget" }]
|} }""".stripMargin
)
assert(processing("buildTarget/scalaTestClasses"))
Expand Down Expand Up @@ -305,9 +307,10 @@ object BuildServerTest extends AbstractServerTest {

test("buildTarget/resources") { _ =>
val buildTarget = buildTargetUri("util", "Compile")
val badBuildTarget = buildTargetUri("badBuildTarget", "Compile")
svr.sendJsonRpc(
s"""{ "jsonrpc": "2.0", "id": "96", "method": "buildTarget/resources", "params": {
| "targets": [{ "uri": "$buildTarget" }]
| "targets": [{ "uri": "$buildTarget" }, { "uri": "$badBuildTarget" }]
|} }""".stripMargin
)
assert(processing("buildTarget/resources"))
Expand Down

0 comments on commit 60cc912

Please sign in to comment.