Skip to content

Commit 5d132f7

Browse files
committed
Add operation retries to prevent unnecessary failures when multiple Scala CLI instances are run on the same project in parallel
1 parent b66919c commit 5d132f7

File tree

9 files changed

+243
-97
lines changed

9 files changed

+243
-97
lines changed

modules/build/src/main/scala/scala/build/Bloop.scala

+27-21
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ import java.io.{File, IOException}
1212
import scala.annotation.tailrec
1313
import scala.build.EitherCps.{either, value}
1414
import scala.build.errors.{BuildException, ModuleFormatError}
15-
import scala.build.internal.CsLoggerUtil._
15+
import scala.build.internal.CsLoggerUtil.*
16+
import scala.concurrent.ExecutionException
1617
import scala.concurrent.duration.FiniteDuration
17-
import scala.jdk.CollectionConverters._
18+
import scala.jdk.CollectionConverters.*
1819

1920
object Bloop {
2021

@@ -35,32 +36,37 @@ object Bloop {
3536
logger: Logger,
3637
buildTargetsTimeout: FiniteDuration
3738
): Either[Throwable, Boolean] =
38-
try {
39-
logger.debug("Listing BSP build targets")
40-
val results = buildServer.workspaceBuildTargets()
41-
.get(buildTargetsTimeout.length, buildTargetsTimeout.unit)
42-
val buildTargetOpt = results.getTargets.asScala.find(_.getDisplayName == projectName)
39+
try retry()(logger) {
40+
logger.debug("Listing BSP build targets")
41+
val results = buildServer.workspaceBuildTargets()
42+
.get(buildTargetsTimeout.length, buildTargetsTimeout.unit)
43+
val buildTargetOpt = results.getTargets.asScala.find(_.getDisplayName == projectName)
4344

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

51-
logger.debug(s"Compiling $projectName with Bloop")
52-
val compileRes = buildServer.buildTargetCompile(
53-
new bsp4j.CompileParams(List(buildTarget.getId).asJava)
54-
).get()
52+
logger.debug(s"Compiling $projectName with Bloop")
53+
val compileRes = buildServer.buildTargetCompile(
54+
new bsp4j.CompileParams(List(buildTarget.getId).asJava)
55+
).get()
5556

56-
val success = compileRes.getStatusCode == bsp4j.StatusCode.OK
57-
logger.debug(if (success) "Compilation succeeded" else "Compilation failed")
58-
Right(success)
59-
}
57+
val success = compileRes.getStatusCode == bsp4j.StatusCode.OK
58+
logger.debug(if (success) "Compilation succeeded" else "Compilation failed")
59+
Right(success)
60+
}
6061
catch {
6162
case ex @ BrokenPipeInCauses(e) =>
6263
logger.debug(s"Caught $ex while exchanging with Bloop server, assuming Bloop server exited")
6364
Left(ex)
65+
case ex: ExecutionException =>
66+
logger.debug(
67+
s"Caught $ex while exchanging with Bloop server, you may consider restarting the build server"
68+
)
69+
Left(ex)
6470
}
6571

6672
def bloopClassPath(

modules/build/src/main/scala/scala/build/Build.scala

+6-4
Original file line numberDiff line numberDiff line change
@@ -50,16 +50,17 @@ object Build {
5050
output: os.Path,
5151
diagnostics: Option[Seq[(Either[String, os.Path], bsp4j.Diagnostic)]],
5252
generatedSources: Seq[GeneratedSource],
53-
isPartial: Boolean
53+
isPartial: Boolean,
54+
logger: Logger
5455
) extends Build {
5556
def success: Boolean = true
5657
def successfulOpt: Some[this.type] = Some(this)
5758
def outputOpt: Some[os.Path] = Some(output)
5859
def dependencyClassPath: Seq[os.Path] = sources.resourceDirs ++ artifacts.classPath
5960
def fullClassPath: Seq[os.Path] = Seq(output) ++ dependencyClassPath
60-
private lazy val mainClassesFoundInProject: Seq[String] = MainClass.find(output).sorted
61+
private lazy val mainClassesFoundInProject: Seq[String] = MainClass.find(output, logger).sorted
6162
private lazy val mainClassesFoundOnExtraClasspath: Seq[String] =
62-
options.classPathOptions.extraClassPath.flatMap(MainClass.find).sorted
63+
options.classPathOptions.extraClassPath.flatMap(MainClass.find(_, logger)).sorted
6364
private lazy val mainClassesFoundInUserExtraDependencies: Seq[String] =
6465
artifacts.jarsForUserExtraDependencies.flatMap(MainClass.findInDependency).sorted
6566
def foundMainClasses(): Seq[String] = {
@@ -1184,7 +1185,8 @@ object Build {
11841185
classesDir0,
11851186
buildClient.diagnostics,
11861187
generatedSources,
1187-
partial
1188+
partial,
1189+
logger
11881190
)
11891191
else
11901192
Failed(

modules/build/src/main/scala/scala/build/compiler/BloopCompilerMaker.scala

+20-11
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ package scala.build.compiler
33
import bloop.rifle.{BloopRifleConfig, BloopServer, BloopThreads}
44
import ch.epfl.scala.bsp4j.BuildClient
55

6-
import scala.build.Logger
76
import scala.build.errors.{BuildException, FetchingDependenciesError, Severity}
87
import scala.build.internal.Constants
98
import scala.build.internal.util.WarningMessages
109
import scala.build.options.BuildOptions
10+
import scala.build.{Logger, retry}
1111
import scala.concurrent.duration.DurationInt
1212
import scala.util.Try
1313

@@ -37,16 +37,25 @@ final class BloopCompilerMaker(
3737
case Right(config) =>
3838
val createBuildServer =
3939
() =>
40-
BloopServer.buildServer(
41-
config,
42-
"scala-cli",
43-
Constants.version,
44-
workspace.toNIO,
45-
classesDir.toNIO,
46-
buildClient,
47-
threads,
48-
logger.bloopRifleLogger
49-
)
40+
// retrying here in case a number of Scala CLI processes are started at the same time
41+
// and they all try to connect to the server / spawn a new server
42+
// otherwise, users may run into one of:
43+
// - libdaemonjvm.client.ConnectError$ZombieFound
44+
// - Caught java.lang.RuntimeException: Fatal error, could not spawn Bloop: not running
45+
// - java.lang.RuntimeException: Bloop BSP connection in (...) was unexpectedly closed or bloop didn't start.
46+
// if a sufficiently large number of processes was started, this may happen anyway, of course
47+
retry(if offline then 1 else 3)(logger) {
48+
BloopServer.buildServer(
49+
config,
50+
"scala-cli",
51+
Constants.version,
52+
workspace.toNIO,
53+
classesDir.toNIO,
54+
buildClient,
55+
threads,
56+
logger.bloopRifleLogger
57+
)
58+
}
5059

5160
val res = Try(new BloopCompiler(createBuildServer, 20.seconds, strictBloopJsonCheck))
5261
.toEither

modules/build/src/main/scala/scala/build/internal/MainClass.scala

+49-22
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import org.objectweb.asm
44
import org.objectweb.asm.ClassReader
55

66
import java.io.{ByteArrayInputStream, InputStream}
7+
import java.nio.file.NoSuchFileException
78
import java.util.jar.{Attributes, JarFile, JarInputStream, Manifest}
89
import java.util.zip.ZipEntry
910

1011
import scala.build.input.Element
1112
import scala.build.internal.zip.WrappedZipInputStream
13+
import scala.build.{Logger, retry}
1214

1315
object MainClass {
1416

@@ -44,29 +46,54 @@ object MainClass {
4446
if (foundMainClass) nameOpt else None
4547
}
4648

47-
def findInClass(path: os.Path): Iterator[String] =
48-
findInClass(os.read.inputStream(path))
49-
def findInClass(is: InputStream): Iterator[String] =
49+
private def findInClass(path: os.Path, logger: Logger): Iterator[String] =
5050
try {
51-
val reader = new ClassReader(is)
52-
val checker = new MainMethodChecker
53-
reader.accept(checker, 0)
54-
checker.mainClassOpt.iterator
51+
val is = retry()(logger)(os.read.inputStream(path))
52+
findInClass(is, logger)
53+
}
54+
catch {
55+
case e: NoSuchFileException =>
56+
e.getStackTrace.foreach(ste => logger.debug(ste.toString))
57+
logger.log(s"Class file $path not found: $e")
58+
logger.log("Are you trying to run too many builds at once? Trying to recover...")
59+
Iterator.empty
60+
}
61+
private def findInClass(is: InputStream, logger: Logger): Iterator[String] =
62+
try retry()(logger) {
63+
val reader = new ClassReader(is)
64+
val checker = new MainMethodChecker
65+
reader.accept(checker, 0)
66+
checker.mainClassOpt.iterator
67+
}
68+
catch {
69+
case e: ArrayIndexOutOfBoundsException =>
70+
e.getStackTrace.foreach(ste => logger.debug(ste.toString))
71+
logger.log(s"Class input stream could not be created: $e")
72+
logger.log("Are you trying to run too many builds at once? Trying to recover...")
73+
Iterator.empty
5574
}
5675
finally is.close()
5776

58-
def findInJar(path: os.Path): Iterator[String] = {
59-
val content = os.read.bytes(path)
60-
val jarInputStream = WrappedZipInputStream.create(new ByteArrayInputStream(content))
61-
jarInputStream.entries().flatMap(ent =>
62-
if !ent.isDirectory && ent.getName.endsWith(".class") then {
63-
val content = jarInputStream.readAllBytes()
64-
val inputStream = new ByteArrayInputStream(content)
65-
findInClass(inputStream)
77+
private def findInJar(path: os.Path, logger: Logger): Iterator[String] =
78+
try retry()(logger) {
79+
val content = os.read.bytes(path)
80+
val jarInputStream = WrappedZipInputStream.create(new ByteArrayInputStream(content))
81+
jarInputStream.entries().flatMap(ent =>
82+
if !ent.isDirectory && ent.getName.endsWith(".class") then {
83+
val content = jarInputStream.readAllBytes()
84+
val inputStream = new ByteArrayInputStream(content)
85+
findInClass(inputStream, logger)
86+
}
87+
else Iterator.empty
88+
)
6689
}
67-
else Iterator.empty
68-
)
69-
}
90+
catch {
91+
case e: NoSuchFileException =>
92+
logger.debugStackTrace(e)
93+
logger.log(s"JAR file $path not found: $e, trying to recover...")
94+
logger.log("Are you trying to run too many builds at once? Trying to recover...")
95+
Iterator.empty
96+
}
7097

7198
def findInDependency(jar: os.Path): Option[String] =
7299
jar match {
@@ -79,19 +106,19 @@ object MainClass {
79106
case _ => None
80107
}
81108

82-
def find(output: os.Path): Seq[String] =
109+
def find(output: os.Path, logger: Logger): Seq[String] =
83110
output match {
84111
case o if os.isFile(o) && o.last.endsWith(".class") =>
85-
findInClass(o).toVector
112+
findInClass(o, logger).toVector
86113
case o if os.isFile(o) && o.last.endsWith(".jar") =>
87-
findInJar(o).toVector
114+
findInJar(o, logger).toVector
88115
case o if os.isDir(o) =>
89116
os.walk(o)
90117
.iterator
91118
.filter(os.isFile)
92119
.flatMap {
93120
case classFilePath if classFilePath.last.endsWith(".class") =>
94-
findInClass(classFilePath)
121+
findInClass(classFilePath, logger)
95122
case _ => Iterator.empty
96123
}
97124
.toVector
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package scala.build
2+
3+
import scala.annotation.tailrec
4+
import scala.concurrent.duration.{DurationInt, FiniteDuration}
5+
import scala.util.Random
6+
7+
def retry[T](
8+
maxAttempts: Int = 3,
9+
waitDuration: FiniteDuration = 1.seconds,
10+
variableWaitDelayInMs: Int = 500
11+
)(logger: Logger)(
12+
run: => T
13+
): T = {
14+
@tailrec
15+
def helper(count: Int): T =
16+
try run
17+
catch {
18+
case t: Throwable =>
19+
if count >= maxAttempts then throw t
20+
else
21+
logger.debugStackTrace(t)
22+
val variableDelay = Random.between(0, variableWaitDelayInMs + 1).milliseconds
23+
val currentWaitDuration = waitDuration + variableDelay
24+
logger.log(s"Caught $t, trying again in $currentWaitDuration")
25+
Thread.sleep(currentWaitDuration.toMillis)
26+
helper(count + 1)
27+
}
28+
29+
helper(1)
30+
}

modules/build/src/main/scala/scala/build/postprocessing/AsmPositionUpdater.scala

+31-14
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ package scala.build.postprocessing
22

33
import org.objectweb.asm
44

5-
import scala.build.{Logger, Os}
5+
import java.io
6+
import java.nio.file.{FileAlreadyExistsException, NoSuchFileException}
7+
8+
import scala.build.{Logger, Os, retry}
69

710
object AsmPositionUpdater {
811

@@ -53,20 +56,34 @@ object AsmPositionUpdater {
5356
.filter(os.isFile(_))
5457
.filter(_.last.endsWith(".class"))
5558
.foreach { path =>
56-
val is = os.read.inputStream(path)
57-
val updateByteCodeOpt =
58-
try {
59-
val reader = new asm.ClassReader(is)
60-
val writer = new asm.ClassWriter(reader, 0)
61-
val checker = new LineNumberTableClassVisitor(mappings, writer)
62-
reader.accept(checker, 0)
63-
if (checker.mappedStuff) Some(writer.toByteArray)
64-
else None
59+
try retry()(logger) {
60+
val is = os.read.inputStream(path)
61+
val updateByteCodeOpt =
62+
try retry()(logger) {
63+
val reader = new asm.ClassReader(is)
64+
val writer = new asm.ClassWriter(reader, 0)
65+
val checker = new LineNumberTableClassVisitor(mappings, writer)
66+
reader.accept(checker, 0)
67+
if checker.mappedStuff then Some(writer.toByteArray) else None
68+
}
69+
catch {
70+
case e: ArrayIndexOutOfBoundsException =>
71+
e.getStackTrace.foreach(ste => logger.debug(ste.toString))
72+
logger.log(s"Error while processing ${path.relativeTo(Os.pwd)}: $e.")
73+
logger.log("Are you trying to run too many builds at once? Trying to recover...")
74+
None
75+
}
76+
finally is.close()
77+
for (b <- updateByteCodeOpt) {
78+
logger.debug(s"Overwriting ${path.relativeTo(Os.pwd)}")
79+
os.write.over(path, b)
80+
}
6581
}
66-
finally is.close()
67-
for (b <- updateByteCodeOpt) {
68-
logger.debug(s"Overwriting ${path.relativeTo(Os.pwd)}")
69-
os.write.over(path, b)
82+
catch {
83+
case e: (NoSuchFileException | FileAlreadyExistsException | ArrayIndexOutOfBoundsException) =>
84+
logger.debugStackTrace(e)
85+
logger.log(s"Error while processing ${path.relativeTo(Os.pwd)}: $e")
86+
logger.log("Are you trying to run too many builds at once? Trying to recover...")
7087
}
7188
}
7289
}

0 commit comments

Comments
 (0)