Skip to content

Commit

Permalink
Restart Bloop server if it exited
Browse files Browse the repository at this point in the history
Can happen during watch mode in particular…
  • Loading branch information
alexarchambault committed Jan 11, 2023
1 parent 22814fd commit c8bbe73
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 46 deletions.
60 changes: 39 additions & 21 deletions modules/build/src/main/scala/scala/build/Bloop.scala
Expand Up @@ -6,9 +6,11 @@ import coursier.util.Task
import dependency.parser.ModuleParser
import dependency.{AnyDependency, DependencyLike, ScalaParameters, ScalaVersion}

import java.io.File
import java.io.{File, IOException}

import scala.annotation.tailrec
import scala.build.EitherCps.{either, value}
import scala.build.bloop.BuildServer
import scala.build.blooprifle.BloopRifleConfig
import scala.build.errors.{BuildException, ModuleFormatError}
import scala.build.internal.CsLoggerUtil._
Expand All @@ -17,33 +19,49 @@ import scala.jdk.CollectionConverters._

object Bloop {

private object BrokenPipeInCauses {
@tailrec
def unapply(ex: Throwable): Option[IOException] =
ex match {
case null => None
case ex: IOException if ex.getMessage == "Broken pipe" => Some(ex)
case ex: IOException if ex.getMessage == "Connection reset by peer" => Some(ex)
case _ => unapply(ex.getCause)
}
}

def compile(
projectName: String,
bloopServer: bloop.BloopServer,
buildServer: BuildServer,
logger: Logger,
buildTargetsTimeout: FiniteDuration
): Boolean = {
): Either[Throwable, Boolean] =
try {
logger.debug("Listing BSP build targets")
val results = buildServer.workspaceBuildTargets()
.get(buildTargetsTimeout.length, buildTargetsTimeout.unit)
val buildTargetOpt = results.getTargets.asScala.find(_.getDisplayName == projectName)

logger.debug("Listing BSP build targets")
val results = bloopServer.server.workspaceBuildTargets()
.get(buildTargetsTimeout.length, buildTargetsTimeout.unit)
val buildTargetOpt = results.getTargets.asScala.find(_.getDisplayName == projectName)

val buildTarget = buildTargetOpt.getOrElse {
throw new Exception(
s"Expected to find project '$projectName' in build targets (only got ${results.getTargets.asScala.map("'" + _.getDisplayName + "'").mkString(", ")})"
)
}
val buildTarget = buildTargetOpt.getOrElse {
throw new Exception(
s"Expected to find project '$projectName' in build targets (only got ${results.getTargets.asScala.map("'" + _.getDisplayName + "'").mkString(", ")})"
)
}

logger.debug(s"Compiling $projectName with Bloop")
val compileRes = bloopServer.server.buildTargetCompile(
new bsp4j.CompileParams(List(buildTarget.getId).asJava)
).get()
logger.debug(s"Compiling $projectName with Bloop")
val compileRes = buildServer.buildTargetCompile(
new bsp4j.CompileParams(List(buildTarget.getId).asJava)
).get()

val success = compileRes.getStatusCode == bsp4j.StatusCode.OK
logger.debug(if (success) "Compilation succeeded" else "Compilation failed")
success
}
val success = compileRes.getStatusCode == bsp4j.StatusCode.OK
logger.debug(if (success) "Compilation succeeded" else "Compilation failed")
Right(success)
}
catch {
case ex @ BrokenPipeInCauses(e) =>
logger.debug(s"Caught $ex while exchanging with Bloop server, assuming Bloop server exited")
Left(ex)
}

def bloopClassPath(
dep: AnyDependency,
Expand Down
24 changes: 13 additions & 11 deletions modules/build/src/main/scala/scala/build/bsp/BspImpl.scala
Expand Up @@ -361,18 +361,20 @@ final class BspImpl(
): BloopSession = {
val logger = reloadableOptions.logger
val buildOptions = reloadableOptions.buildOptions
val bloopServer = BloopServer.buildServer(
reloadableOptions.bloopRifleConfig,
"scala-cli",
Constants.version,
(inputs.workspace / Constants.workspaceDirName).toNIO,
Build.classesRootDir(inputs.workspace, inputs.projectName).toNIO,
localClient,
threads.buildThreads.bloop,
logger.bloopRifleLogger
)
val createBloopServer =
() =>
BloopServer.buildServer(
reloadableOptions.bloopRifleConfig,
"scala-cli",
Constants.version,
(inputs.workspace / Constants.workspaceDirName).toNIO,
Build.classesRootDir(inputs.workspace, inputs.projectName).toNIO,
localClient,
threads.buildThreads.bloop,
logger.bloopRifleLogger
)
val remoteServer = new BloopCompiler(
bloopServer,
createBloopServer,
20.seconds,
strictBloopJsonCheck = buildOptions.internal.strictBloopJsonCheckOrDefault
)
Expand Down
@@ -1,13 +1,19 @@
package scala.build.compiler

import scala.annotation.tailrec
import scala.build.{Bloop, Logger, Position, Positioned, Project, bloop}
import scala.concurrent.duration.FiniteDuration

final class BloopCompiler(
val bloopServer: bloop.BloopServer,
createServer: () => bloop.BloopServer,
buildTargetsTimeout: FiniteDuration,
strictBloopJsonCheck: Boolean
) extends ScalaCompiler {
private var currentBloopServer: bloop.BloopServer =
createServer()
def bloopServer: bloop.BloopServer =
currentBloopServer

def jvmVersion: Option[Positioned[Int]] =
Some(
Positioned(
Expand All @@ -25,8 +31,26 @@ final class BloopCompiler(
def compile(
project: Project,
logger: Logger
): Boolean =
Bloop.compile(project.projectName, bloopServer, logger, buildTargetsTimeout)
): Boolean = {
@tailrec
def helper(remainingAttempts: Int): Boolean =
Bloop.compile(project.projectName, bloopServer.server, logger, buildTargetsTimeout) match {
case Right(res) => res
case Left(ex) =>
if (remainingAttempts > 1) {
logger.debug(s"Seems Bloop server exited (got $ex), trying to restart one")
currentBloopServer = createServer()
helper(remainingAttempts - 1)
}
else
throw new Exception(
"Seems compilation server exited, and wasn't able to restart one",
ex
)
}

helper(2)
}

def shutdown(): Unit =
bloopServer.shutdown()
Expand Down
Expand Up @@ -19,16 +19,18 @@ final class BloopCompilerMaker(
buildClient: BuildClient,
logger: Logger
): BloopCompiler = {
val buildServer = BloopServer.buildServer(
config,
"scala-cli",
Constants.version,
workspace.toNIO,
classesDir.toNIO,
buildClient,
threads,
logger.bloopRifleLogger
)
new BloopCompiler(buildServer, 20.seconds, strictBloopJsonCheck)
val createBuildServer =
() =>
BloopServer.buildServer(
config,
"scala-cli",
Constants.version,
workspace.toNIO,
classesDir.toNIO,
buildClient,
threads,
logger.bloopRifleLogger
)
new BloopCompiler(createBuildServer, 20.seconds, strictBloopJsonCheck)
}
}
Expand Up @@ -3,6 +3,8 @@ package scala.cli.integration
import com.eed3si9n.expecty.Expecty.expect

import scala.cli.integration.util.BloopUtil
import scala.concurrent.duration.Duration
import scala.concurrent.{Await, ExecutionContext, Future}

class BloopTests extends ScalaCliSuite {

Expand Down Expand Up @@ -138,4 +140,42 @@ class BloopTests extends ScalaCliSuite {
.call(cwd = root / Constants.workspaceDirName)
}
}

test("Restart Bloop server while watching") {
TestUtil.withThreadPool("bloop-restart-test", 2) { pool =>
val timeout = Duration("20 seconds")
def readLine(stream: os.SubProcess.OutputStream): String = {
implicit val ec = ExecutionContext.fromExecutorService(pool)
val readLineF = Future {
stream.readLine()
}
Await.result(readLineF, timeout)
}
def content(message: String) =
s"""object Hello {
| def main(args: Array[String]): Unit =
| println("$message")
|}
|""".stripMargin
val sourcePath = os.rel / "Hello.scala"
val inputs = TestInputs(
sourcePath -> content("Hello")
)
inputs.fromRoot { root =>
val proc = os.proc(TestUtil.cli, "run", "-w", ".")
.spawn(cwd = root)
val firstLine = readLine(proc.stdout)
expect(firstLine == "Hello")

os.proc(TestUtil.cli, "bloop", "exit")
.call(cwd = root)

os.write.over(root / sourcePath, content("Foo"))
val secondLine = readLine(proc.stdout)
expect(secondLine == "Foo")

proc.destroy()
}
}
}
}
Expand Up @@ -76,6 +76,16 @@ object TestUtil {

def threadPool(prefix: String, size: Int): ExecutorService =
Executors.newFixedThreadPool(size, daemonThreadFactory(prefix))
def withThreadPool[T](prefix: String, size: Int)(f: ExecutorService => T): T = {
var pool: ExecutorService = null
try {
pool = threadPool(prefix, size)
f(pool)
}
finally
if (pool != null)
pool.shutdown()
}

def scheduler(prefix: String): ScheduledExecutorService =
Executors.newSingleThreadScheduledExecutor(daemonThreadFactory(prefix))
Expand Down

0 comments on commit c8bbe73

Please sign in to comment.