Skip to content

Commit 3063b48

Browse files
Add a logger to Retry, and make the integration tests Retry effective (#4793)
This adds a `logger: String => Unit` argument to `Retry`, so that we can know in the logs (CI logs say) if a retry did happen. Two helpers are added to help users pass that new argument: `Retry.ctxLogger` that uses `Task.ctx`, and `Retry.printStream` if one wants to log with `System.err` for example. Also, this moves the `Retry` of integration tests from `UtestIntegrationTestSuite` to `IntegrationTestSuite#integrationTest`. In the former location, it was wrapping a `Future`, which `Retry` doesn't handle, so that the retry actually had no effect I think. Lastly, this ignores `TimeoutException`s for example tests that have a `ignoreErrorsOnCI` file (introduced in #4751).
1 parent 353f304 commit 3063b48

File tree

7 files changed

+79
-41
lines changed

7 files changed

+79
-41
lines changed

main/util/src/mill/util/Retry.scala

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
package mill.util
22

3+
import mill.api.Ctx
4+
5+
import java.io.{ByteArrayOutputStream, PrintStream}
6+
import java.nio.charset.StandardCharsets
37
import java.util.concurrent.TimeUnit
8+
49
import scala.concurrent.duration.Duration
510
import scala.concurrent.{Await, Promise}
611

712
/**
813
* Generic retry functionality
914
*
15+
* @param logger Method to log messages upon failure
1016
* @param count How many times to retry before giving up
1117
* @param backoffMillis What is the initial backoff time
1218
* @param backoffMultiplier How much to multiply the initial backoff each time
@@ -21,6 +27,7 @@ import scala.concurrent.{Await, Promise}
2127
* [[t]] fails more than [[count]] times
2228
*/
2329
case class Retry(
30+
logger: String => Unit,
2431
count: Int = 5,
2532
backoffMillis: Long = 10,
2633
backoffMultiplier: Double = 2.0,
@@ -46,6 +53,8 @@ case class Retry(
4653
}
4754
} catch {
4855
case e: Throwable if retryCount < count && filter(retryCount + 1, e) =>
56+
logger(Retry.printException(e))
57+
logger(s"Attempt ${retryCount + 1} failed, waiting $currentBackoffMillis ms")
4958
Thread.sleep(currentBackoffMillis)
5059
rec(retryCount + 1, (currentBackoffMillis * backoffMultiplier).toInt)
5160
}
@@ -54,3 +63,20 @@ case class Retry(
5463
rec(0, backoffMillis)
5564
}
5665
}
66+
67+
object Retry {
68+
69+
/** Use this logger to log from a Mill task */
70+
def ctxLogger(implicit ctx: Ctx.Log): String => Unit =
71+
ctx.log.warn(_)
72+
73+
/** Use this logger to log with a `PrintStream`, such as `System.err` */
74+
def printStreamLogger(printStream: PrintStream): String => Unit =
75+
printStream.println(_)
76+
77+
private def printException(ex: Throwable): String = {
78+
val baos = new ByteArrayOutputStream
79+
ex.printStackTrace(new PrintStream(baos, true, StandardCharsets.UTF_8))
80+
new String(baos.toByteArray, StandardCharsets.UTF_8)
81+
}
82+
}

main/util/test/src/mill/util/RetryTests.scala

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ object RetryTests extends TestSuite {
77
test("fail") {
88
var count = 0
99
try {
10-
Retry() {
10+
Retry(Retry.printStreamLogger(System.err)) {
1111
count += 1
1212
throw new Exception("boom")
1313
}
@@ -20,7 +20,7 @@ object RetryTests extends TestSuite {
2020
}
2121
test("succeed") {
2222
var count = 0
23-
Retry() {
23+
Retry(Retry.printStreamLogger(System.err)) {
2424
count += 1
2525
if (count < 3) throw new Exception("boom")
2626
}
@@ -30,6 +30,7 @@ object RetryTests extends TestSuite {
3030
var count = 0
3131
try {
3232
Retry(
33+
Retry.printStreamLogger(System.err),
3334
filter = {
3435
case (i, ex: RuntimeException) => true
3536
case _ => false
@@ -49,7 +50,7 @@ object RetryTests extends TestSuite {
4950
test("fail") {
5051
var count = 0
5152
try {
52-
Retry(timeoutMillis = 100) {
53+
Retry(Retry.printStreamLogger(System.err), timeoutMillis = 100) {
5354
count += 1
5455
Thread.sleep(1000)
5556
}
@@ -62,7 +63,7 @@ object RetryTests extends TestSuite {
6263
}
6364
test("success") {
6465
var count = 0
65-
Retry(timeoutMillis = 100) {
66+
Retry(Retry.printStreamLogger(System.err), timeoutMillis = 100) {
6667
count += 1
6768
if (count < 3) Thread.sleep(1000)
6869
}

scalalib/worker/src/mill/scalalib/worker/ZincWorkerImpl.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ class ZincWorkerImpl(
231231
) { (compilers: Compilers) =>
232232
// Not sure why dotty scaladoc is flaky, but add retries to workaround it
233233
// https://github.com/com-lihaoyi/mill/issues/4556
234-
mill.util.Retry(count = 2) {
234+
mill.util.Retry(mill.util.Retry.ctxLogger, count = 2) {
235235
if (
236236
ZincWorkerUtil.isDotty(scalaVersion) || ZincWorkerUtil.isScala3Milestone(scalaVersion)
237237
) {

testkit/src/mill/testkit/IntegrationTestSuite.scala

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package mill.testkit
22

3+
import mill.util.Retry
34
import os.Path
45

6+
import scala.concurrent.duration.DurationInt
7+
58
trait IntegrationTestSuite {
69

710
/**
@@ -39,15 +42,21 @@ trait IntegrationTestSuite {
3942
* given [[block]].
4043
*/
4144
def integrationTest[T](block: IntegrationTester => T): T = {
42-
val tester = new IntegrationTester(
43-
clientServerMode,
44-
workspaceSourcePath,
45-
millExecutable,
46-
debugLog,
47-
baseWorkspacePath = os.pwd,
48-
propagateJavaHome = propagateJavaHome
49-
)
50-
try block(tester)
51-
finally tester.close()
45+
Retry(
46+
Retry.printStreamLogger(System.err),
47+
count = if (sys.env.contains("CI")) 1 else 0,
48+
timeoutMillis = 10.minutes.toMillis
49+
) {
50+
val tester = new IntegrationTester(
51+
clientServerMode,
52+
workspaceSourcePath,
53+
millExecutable,
54+
debugLog,
55+
baseWorkspacePath = os.pwd,
56+
propagateJavaHome = propagateJavaHome
57+
)
58+
try block(tester)
59+
finally tester.close()
60+
}
5261
}
5362
}

testkit/src/mill/testkit/IntegrationTesterBase.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ trait IntegrationTesterBase {
3939
def initWorkspace(): Unit = {
4040
println(s"Copying integration test sources from $workspaceSourcePath to $workspacePath")
4141
os.makeDir.all(workspacePath)
42-
Retry() {
42+
Retry(Retry.printStreamLogger(System.err)) {
4343
val tmp = os.temp.dir()
4444
val outDir = os.Path(out, workspacePath)
4545
if (os.exists(outDir)) os.move.into(outDir, tmp)

testkit/src/mill/testkit/UtestExampleTestSuite.scala

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package mill.testkit
33
import mill.util.Retry
44
import utest._
55

6+
import java.util.concurrent.TimeoutException
7+
68
import scala.concurrent.duration.DurationInt
79

810
object UtestExampleTestSuite extends TestSuite {
@@ -13,16 +15,31 @@ object UtestExampleTestSuite extends TestSuite {
1315
val tests: Tests = Tests {
1416

1517
test("exampleTest") {
16-
Retry(
17-
count = if (sys.env.contains("CI")) 1 else 0,
18-
timeoutMillis = 15.minutes.toMillis
19-
) {
20-
ExampleTester.run(
21-
clientServerMode,
22-
workspaceSourcePath,
23-
millExecutable
24-
)
25-
}
18+
def run() =
19+
Retry(
20+
Retry.printStreamLogger(System.err),
21+
count = if (sys.env.contains("CI")) 1 else 0,
22+
timeoutMillis = 15.minutes.toMillis
23+
) {
24+
ExampleTester.run(
25+
clientServerMode,
26+
workspaceSourcePath,
27+
millExecutable
28+
)
29+
}
30+
31+
val ignoreErrors = System.getenv("CI") != null &&
32+
os.exists(workspaceSourcePath / "ignoreErrorsOnCI")
33+
if (ignoreErrors)
34+
try run()
35+
catch {
36+
case _: TimeoutException =>
37+
System.err.println(
38+
s"Found ignoreErrorsOnCI under $workspaceSourcePath, ignoring timeout exception"
39+
)
40+
}
41+
else
42+
run()
2643
}
2744
}
2845
}
Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
package mill.testkit
22

3-
import mill.util.Retry
4-
import scala.concurrent.duration.DurationInt
5-
import scala.concurrent.{ExecutionContext, Future}
6-
73
abstract class UtestIntegrationTestSuite extends utest.TestSuite with IntegrationTestSuite {
84
protected def workspaceSourcePath: os.Path = os.Path(sys.env("MILL_TEST_RESOURCE_DIR"))
95
protected def clientServerMode: Boolean = sys.env("MILL_INTEGRATION_SERVER_MODE").toBoolean
@@ -13,15 +9,4 @@ abstract class UtestIntegrationTestSuite extends utest.TestSuite with Integratio
139
sys.env("MILL_INTEGRATION_IS_PACKAGED_LAUNCHER").toBoolean
1410
protected def millExecutable: os.Path =
1511
os.Path(System.getenv("MILL_INTEGRATION_LAUNCHER"), os.pwd)
16-
17-
override def utestWrap(path: Seq[String], runBody: => Future[Any])(implicit
18-
ec: ExecutionContext
19-
): Future[Any] = {
20-
Retry(
21-
count = if (sys.env.contains("CI")) 1 else 0,
22-
timeoutMillis = 10.minutes.toMillis
23-
) {
24-
super.utestWrap(path, runBody)
25-
}
26-
}
2712
}

0 commit comments

Comments
 (0)