Skip to content

Commit 4ff0171

Browse files
committed
Retry IO access to compilation outputs and try to recover when a file is unavailable
1 parent 746ecb8 commit 4ff0171

File tree

6 files changed

+199
-74
lines changed

6 files changed

+199
-74
lines changed

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

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -52,16 +52,17 @@ object Build {
5252
output: os.Path,
5353
diagnostics: Option[Seq[(Either[String, os.Path], bsp4j.Diagnostic)]],
5454
generatedSources: Seq[GeneratedSource],
55-
isPartial: Boolean
55+
isPartial: Boolean,
56+
logger: Logger
5657
) extends Build {
5758
def success: Boolean = true
5859
def successfulOpt: Some[this.type] = Some(this)
5960
def outputOpt: Some[os.Path] = Some(output)
6061
def dependencyClassPath: Seq[os.Path] = sources.resourceDirs ++ artifacts.classPath
6162
def fullClassPath: Seq[os.Path] = Seq(output) ++ dependencyClassPath
62-
private lazy val mainClassesFoundInProject: Seq[String] = MainClass.find(output).sorted
63+
private lazy val mainClassesFoundInProject: Seq[String] = MainClass.find(output, logger).sorted
6364
private lazy val mainClassesFoundOnExtraClasspath: Seq[String] =
64-
options.classPathOptions.extraClassPath.flatMap(MainClass.find).sorted
65+
options.classPathOptions.extraClassPath.flatMap(MainClass.find(_, logger)).sorted
6566
private lazy val mainClassesFoundInUserExtraDependencies: Seq[String] =
6667
artifacts.jarsForUserExtraDependencies.flatMap(MainClass.findInDependency).sorted
6768
def foundMainClasses(): Seq[String] = {
@@ -1166,27 +1167,28 @@ object Build {
11661167

11671168
if (success)
11681169
Successful(
1169-
inputs,
1170-
options,
1171-
scalaParams,
1172-
scope,
1173-
sources,
1174-
artifacts,
1175-
project,
1176-
classesDir0,
1177-
buildClient.diagnostics,
1178-
generatedSources,
1179-
partial
1170+
inputs = inputs,
1171+
options = options,
1172+
scalaParams = scalaParams,
1173+
scope = scope,
1174+
sources = sources,
1175+
artifacts = artifacts,
1176+
project = project,
1177+
output = classesDir0,
1178+
diagnostics = buildClient.diagnostics,
1179+
generatedSources = generatedSources,
1180+
isPartial = partial,
1181+
logger = logger
11801182
)
11811183
else
11821184
Failed(
1183-
inputs,
1184-
options,
1185-
scope,
1186-
sources,
1187-
artifacts,
1188-
project,
1189-
buildClient.diagnostics
1185+
inputs = inputs,
1186+
options = options,
1187+
scope = scope,
1188+
sources = sources,
1189+
artifacts = artifacts,
1190+
project = project,
1191+
diagnostics = buildClient.diagnostics
11901192
)
11911193
}
11921194

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

Lines changed: 48 additions & 22 deletions
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,53 @@ 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+
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, trying to recover...")
58+
Iterator.empty
59+
}
60+
def findInClass(is: InputStream, logger: Logger): Iterator[String] =
61+
try
62+
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, trying to recover...")
72+
Iterator.empty
5573
}
5674
finally is.close()
5775

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)
76+
def findInJar(path: os.Path, logger: Logger): Iterator[String] =
77+
try
78+
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+
e.getStackTrace.foreach(ste => logger.debug(ste.toString))
93+
logger.log(s"JAR file $path not found: $e, trying to recover...")
94+
Iterator.empty
95+
}
7096

7197
def findInDependency(jar: os.Path): Option[String] =
7298
jar match {
@@ -79,19 +105,19 @@ object MainClass {
79105
case _ => None
80106
}
81107

82-
def find(output: os.Path): Seq[String] =
108+
def find(output: os.Path, logger: Logger): Seq[String] =
83109
output match {
84110
case o if os.isFile(o) && o.last.endsWith(".class") =>
85-
findInClass(o).toVector
111+
findInClass(o, logger).toVector
86112
case o if os.isFile(o) && o.last.endsWith(".jar") =>
87-
findInJar(o).toVector
113+
findInJar(o, logger).toVector
88114
case o if os.isDir(o) =>
89115
os.walk(o)
90116
.iterator
91117
.filter(os.isFile)
92118
.flatMap {
93119
case classFilePath if classFilePath.last.endsWith(".class") =>
94-
findInClass(classFilePath)
120+
findInClass(classFilePath, logger)
95121
case _ => Iterator.empty
96122
}
97123
.toVector
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package scala
2+
3+
import scala.annotation.tailrec
4+
import scala.concurrent.duration.DurationConversions.*
5+
import scala.concurrent.duration.{DurationInt, FiniteDuration}
6+
7+
package object build {
8+
def retry[T](
9+
maxAttempts: Int = 3,
10+
waitDuration: FiniteDuration = 1.seconds
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)
20+
throw new Exception(t)
21+
else {
22+
t.getStackTrace.foreach(ste => logger.debug(ste.toString))
23+
logger.log(s"Caught $t, trying again in $waitDuration")
24+
Thread.sleep(waitDuration.toMillis)
25+
helper(count + 1)
26+
}
27+
}
28+
29+
helper(1)
30+
}
31+
}

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

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
package scala.build.postprocessing
22

33
import org.objectweb.asm
4+
import os.SubProcess.InputStream
45

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

711
object AsmPositionUpdater {
812

@@ -53,15 +57,33 @@ object AsmPositionUpdater {
5357
.filter(os.isFile(_))
5458
.filter(_.last.endsWith(".class"))
5559
.foreach { path =>
56-
val is = os.read.inputStream(path)
60+
val is: io.InputStream =
61+
try retry()(logger)(os.read.inputStream(path))
62+
catch {
63+
case e: NoSuchFileException =>
64+
e.getStackTrace.foreach(ste => logger.debug(ste.toString))
65+
logger.message(
66+
s"Error while processing ${path.relativeTo(Os.pwd)}: $e, trying to recover..."
67+
)
68+
io.InputStream.nullInputStream()
69+
}
5770
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
71+
try
72+
retry()(logger) {
73+
val reader = new asm.ClassReader(is)
74+
val writer = new asm.ClassWriter(reader, 0)
75+
val checker = new LineNumberTableClassVisitor(mappings, writer)
76+
reader.accept(checker, 0)
77+
if (checker.mappedStuff) Some(writer.toByteArray)
78+
else None
79+
}
80+
catch {
81+
case e: ArrayIndexOutOfBoundsException =>
82+
e.getStackTrace.foreach(ste => logger.debug(ste.toString))
83+
logger.log(
84+
s"Error while processing ${path.relativeTo(Os.pwd)}: $e, trying to recover..."
85+
)
86+
None
6587
}
6688
finally is.close()
6789
for (b <- updateByteCodeOpt) {

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

Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package scala.build.postprocessing
22

3+
import java.nio.file.NoSuchFileException
4+
35
import scala.build.internal.Constants
46
import scala.build.options.BuildOptions
57
import scala.build.tastylib.{TastyData, TastyVersions}
6-
import scala.build.{GeneratedSource, Logger}
8+
import scala.build.{GeneratedSource, Logger, retry}
79

810
case object TastyPostProcessor extends PostProcessor {
911

@@ -51,28 +53,39 @@ case object TastyPostProcessor extends PostProcessor {
5153
updatedPaths: Map[String, String]
5254
)(f: os.Path): Unit = {
5355
logger.debug(s"Reading TASTy file $f")
54-
val content = os.read.bytes(f)
55-
TastyData.read(content) match {
56-
case Left(ex) => logger.debug(s"Ignoring exception during TASty postprocessing: $ex")
57-
case Right(data) =>
58-
logger.debug(s"Parsed TASTy file $f")
59-
var updatedOne = false
60-
val updatedData = data.mapNames { n =>
61-
updatedPaths.get(n) match {
62-
case Some(newName) =>
63-
updatedOne = true
64-
newName
65-
case None =>
66-
n
67-
}
68-
}
69-
if (updatedOne) {
70-
logger.debug(
71-
s"Overwriting ${if (f.startsWith(os.pwd)) f.relativeTo(os.pwd) else f}"
72-
)
73-
val updatedContent = TastyData.write(updatedData)
74-
os.write.over(f, updatedContent)
56+
try
57+
retry()(logger) {
58+
val content = os.read.bytes(f)
59+
TastyData.read(content) match {
60+
case Left(ex) => logger.debug(s"Ignoring exception during TASty postprocessing: $ex")
61+
case Right(data) =>
62+
logger.debug(s"Parsed TASTy file $f")
63+
var updatedOne = false
64+
val updatedData = data.mapNames { n =>
65+
updatedPaths.get(n) match {
66+
case Some(newName) =>
67+
updatedOne = true
68+
newName
69+
case None =>
70+
n
71+
}
72+
}
73+
if (updatedOne) {
74+
logger.debug(
75+
s"Overwriting ${if (f.startsWith(os.pwd)) f.relativeTo(os.pwd) else f}"
76+
)
77+
val updatedContent = TastyData.write(updatedData)
78+
os.write.over(f, updatedContent)
79+
}
7580
}
81+
}
82+
catch {
83+
case e: NoSuchFileException =>
84+
e.getStackTrace.foreach(ste => logger.debug(ste.toString))
85+
logger.log(s"Tasty file $f not found: $e, trying to recover...")
86+
case e: ArrayIndexOutOfBoundsException =>
87+
e.getStackTrace.foreach(ste => logger.debug(ste.toString))
88+
logger.log(s"Error while processing $f: $e, trying to recover...")
7689
}
7790
}
7891
}

modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package scala.cli.integration
22

33
import com.eed3si9n.expecty.Expecty.expect
4+
import os.SubProcess
45

56
import java.io.{ByteArrayOutputStream, File}
67
import java.nio.charset.Charset
@@ -2304,4 +2305,34 @@ abstract class RunTestDefinitions
23042305
expect(err.contains(main2))
23052306
}
23062307
}
2308+
2309+
for {
2310+
(input, code) <- Seq(
2311+
os.rel / "script.sc" -> """println(args.mkString(" "))""",
2312+
os.rel / "raw.scala" -> """object Main { def main(args: Array[String]) = println(args.mkString(" ")) }"""
2313+
)
2314+
testInputs = TestInputs(input -> code)
2315+
}
2316+
test(s"run several instances of $input in parallel") {
2317+
testInputs.fromRoot {
2318+
root =>
2319+
val processes: Seq[(SubProcess, Int)] =
2320+
(0 to 20).map { i =>
2321+
os.proc(
2322+
TestUtil.cli,
2323+
"run",
2324+
input.toString(),
2325+
extraOptions,
2326+
"--",
2327+
"iteration",
2328+
i.toString
2329+
)
2330+
.spawn(cwd = root)
2331+
}.zipWithIndex
2332+
processes.foreach { case (p, _) => p.waitFor() }
2333+
processes.foreach { case (p, _) => expect(p.exitCode == 0) }
2334+
processes.foreach { case (p, i) => expect(p.stdout.trim() == s"iteration $i") }
2335+
}
2336+
2337+
}
23072338
}

0 commit comments

Comments
 (0)