Skip to content

Commit 170490b

Browse files
authored
Merge pull request #412 from eed3si9n/wip/retry2
Add Retry.io
2 parents 3e889e3 + 04e9800 commit 170490b

File tree

2 files changed

+93
-5
lines changed

2 files changed

+93
-5
lines changed

io/src/main/scala/sbt/internal/io/Retry.scala

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,33 +20,100 @@ private[sbt] object Retry {
2020
try System.getProperty("sbt.io.retry.limit", defaultLimit.toString).toInt
2121
catch { case NonFatal(_) => defaultLimit }
2222
}
23+
private final val defaultSleepInMillis = 100
24+
25+
/**
26+
* Retry on all non-fatal exceptions that are NOT listed in
27+
* the excludedExceptions list.
28+
*/
2329
private[sbt] def apply[@specialized T](f: => T, excludedExceptions: Class[? <: Throwable]*): T =
2430
apply(f, limit, excludedExceptions: _*)
31+
32+
/**
33+
* Retry on all non-fatal exceptions that are NOT listed in
34+
* the excludedExceptions list.
35+
*/
2536
private[sbt] def apply[@specialized T](
2637
f: => T,
2738
limit: Int,
2839
excludedExceptions: Class[? <: Throwable]*,
29-
): T = apply(f, limit, 100, excludedExceptions: _*)
40+
): T = apply(f, limit, defaultSleepInMillis, excludedExceptions: _*)
41+
42+
/**
43+
* Retry on all non-fatal exceptions that are NOT listed in
44+
* the excludedExceptions list.
45+
*/
3046
private[sbt] def apply[@specialized T](
3147
f: => T,
3248
limit: Int,
3349
sleepInMillis: Long,
3450
excludedExceptions: Class[? <: Throwable]*,
3551
): T = {
36-
require(limit >= 1, "limit must be 1 or higher: was: " + limit)
37-
def filter(e: Throwable): Boolean = excludedExceptions match {
52+
def allowRetry(e: Throwable): Boolean = excludedExceptions match {
3853
case s if s.nonEmpty =>
3954
!excludedExceptions.exists(_.isAssignableFrom(e.getClass))
4055
case _ =>
4156
true
4257
}
58+
impl(limit = limit, sleepInMillis = sleepInMillis)(allowRetry)(f)
59+
}
60+
61+
/**
62+
* Retry on all IOExceptions that are NOT listed in
63+
* the excludedExceptions list.
64+
* Non-IOException will immediately throw.
65+
*/
66+
private[sbt] def io[@specialized A1](f: => A1, excludedExceptions: Class[? <: IOException]*): A1 =
67+
io(f, limit, excludedExceptions: _*)
68+
69+
/**
70+
* Retry on all IOExceptions that are NOT listed in
71+
* the excludedExceptions list.
72+
* Non-IOException will immediately throw.
73+
*/
74+
private[sbt] def io[@specialized A1](
75+
f: => A1,
76+
limit: Int,
77+
excludedExceptions: Class[? <: IOException]*,
78+
): A1 = io(f, limit, defaultSleepInMillis, excludedExceptions: _*)
79+
80+
/**
81+
* Retry on all IOExceptions that are NOT listed in
82+
* the excludedExceptions list.
83+
* Non-IOException will immediately throw.
84+
*/
85+
private[sbt] def io[@specialized A1](
86+
f: => A1,
87+
limit: Int,
88+
sleepInMillis: Long,
89+
excludedExceptions: Class[? <: IOException]*,
90+
): A1 = {
91+
def allowRetry(e: Throwable): Boolean =
92+
e match {
93+
case ioe: IOException =>
94+
excludedExceptions match {
95+
case s if s.nonEmpty =>
96+
!excludedExceptions.exists(_.isAssignableFrom(ioe.getClass))
97+
case _ =>
98+
true
99+
}
100+
case _ => false
101+
}
102+
impl(limit = limit, sleepInMillis = sleepInMillis)(allowRetry)(f)
103+
}
104+
105+
private def impl[@specialized A1](
106+
limit: Int,
107+
sleepInMillis: Long,
108+
)(allowRetry: Throwable => Boolean)(f: => A1): A1 = {
109+
require(limit >= 1, "limit must be 1 or higher: was: " + limit)
43110
var attempt = 1
44111
var firstException: Throwable = null
45112
while (attempt <= limit) {
46113
try {
47114
return f
48115
} catch {
49-
case NonFatal(e) if filter(e) =>
116+
case NonFatal(e) if allowRetry(e) =>
50117
if (firstException == null) firstException = e
51118
// https://github.com/sbt/io/issues/295
52119
// On Windows, we're seeing java.nio.file.AccessDeniedException with sleep(0).

io/src/test/scala/sbt/internal/io/RetrySpec.scala

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,20 @@ object RetrySpec extends verify.BasicTestSuite {
2020
val i = new AtomicInteger()
2121
def throww(): Any = throw new IOException(i.incrementAndGet().toString)
2222
try {
23-
Retry(throww(), limit = 10, sleepInMillis = 0, noExcluded: _*)
23+
Retry(throww(), limit = 10, sleepInMillis = 10, noExcluded: _*)
24+
assert(false)
25+
} catch {
26+
case ioe: IOException =>
27+
assert(ioe.getMessage == "1")
28+
assert(i.get() == 10)
29+
}
30+
}
31+
32+
test("Retry.io should throw first exception after number of failures") {
33+
val i = new AtomicInteger()
34+
def throww(): Any = throw new IOException(i.incrementAndGet().toString)
35+
try {
36+
Retry.io(throww(), limit = 10, sleepInMillis = 10, noExcluded: _*)
2437
assert(false)
2538
} catch {
2639
case ioe: IOException =>
@@ -53,4 +66,12 @@ object RetrySpec extends verify.BasicTestSuite {
5366
Retry(throww())
5467
()
5568
}
69+
70+
test("Retry.io should throw non-IOException") {
71+
def throww(): Any = ???
72+
intercept[NotImplementedError] {
73+
Retry.io(throww())
74+
()
75+
}
76+
}
5677
}

0 commit comments

Comments
 (0)