Skip to content

Commit d1a90ed

Browse files
authored
Rename maxRepeats to maxAttempts, change semantics (#340)
maxRepeats had unclear semantics as to the total number of invocations when using with retry or repeat. maxAttempts clarifies this, and the number of invocations of the function is always capped at the number given to maxAttempts
1 parent 64cbe6a commit d1a90ed

File tree

16 files changed

+99
-88
lines changed

16 files changed

+99
-88
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ supervised {
9393

9494
```scala mdoc:compile-only
9595
def computationR: Int = ???
96-
retry(Schedule.exponentialBackoff(100.millis).maxRepeats(5)
96+
retry(Schedule.exponentialBackoff(100.millis).maxRetries(4)
9797
.jitter().maxInterval(5.minutes))(computationR)
9898
```
9999

core/src/main/scala/ox/scheduling/Schedule.scala

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,20 @@ case class Schedule(intervals: () => LazyList[FiniteDuration], initialDelay: Opt
1919
/** Caps the intervals to the given maximum. */
2020
def maxInterval(max: FiniteDuration): Schedule = copy(intervals = () => intervals().map(_.min(max)))
2121

22-
/** Caps the number of repeats to the given maximum, creating a finite schedule. */
23-
def maxRepeats(max: Int): Schedule = copy(intervals = () => intervals().take(max))
22+
/** Caps the number of attempts to the given maximum, creating a finite schedule. The provided value specifies the total number of
23+
* invocations (attempts) of the operation, including the initial invocation.
24+
*/
25+
def maxAttempts(max: Int): Schedule = copy(intervals = () => intervals().take(max - 1))
26+
27+
/** Caps the number of retries to the given maximum, creating a finite schedule. The provided value specifies the number of retries after
28+
* the initial attempt. The total number of invocations will be `retries + 1`.
29+
*/
30+
def maxRetries(retries: Int): Schedule = maxAttempts(retries + 1)
2431

25-
/** Caps the total delay to the given maximum. The resulting schedule might still be infinite, if the intervals are originally 0. */
26-
def maxRepeatsByCumulativeDelay(upTo: FiniteDuration): Schedule = copy(intervals = () =>
32+
/** Caps the total delay (cumulative time between attempts) to the given maximum. The resulting schedule might still be infinite, if the
33+
* intervals are originally 0.
34+
*/
35+
def maxCumulativeDelay(upTo: FiniteDuration): Schedule = copy(intervals = () =>
2736
val d = intervals()
2837
d
2938
.scanLeft(0.seconds)((cumulative, next) => cumulative + next)

core/src/test/scala/ox/flow/FlowOpsRetryTest.scala

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class FlowOpsRetryTest extends AnyFlatSpec with Matchers with ElapsedTime:
2121
val flow = Flow.fromValues(1, 2, 3)
2222

2323
// when
24-
val result = flow.retry(Schedule.immediate.maxRepeats(3)).runToList()
24+
val result = flow.retry(Schedule.immediate.maxRetries(3)).runToList()
2525

2626
// then
2727
result shouldBe List(1, 2, 3)
@@ -38,7 +38,7 @@ class FlowOpsRetryTest extends AnyFlatSpec with Matchers with ElapsedTime:
3838
}
3939

4040
// when
41-
val result = flow.retry(Schedule.immediate.maxRepeats(maxRetries)).runToList()
41+
val result = flow.retry(Schedule.immediate.maxRetries(maxRetries)).runToList()
4242

4343
// then
4444
result shouldBe List(42)
@@ -58,7 +58,7 @@ class FlowOpsRetryTest extends AnyFlatSpec with Matchers with ElapsedTime:
5858

5959
// when
6060
val (result, elapsedTime) = measure {
61-
flow.retry(Schedule.fixedInterval(interval).maxRepeats(maxRetries)).runToList()
61+
flow.retry(Schedule.fixedInterval(interval).maxRetries(maxRetries)).runToList()
6262
}
6363

6464
// then
@@ -74,7 +74,7 @@ class FlowOpsRetryTest extends AnyFlatSpec with Matchers with ElapsedTime:
7474
val flow = Flow
7575
.fromValues(1, 2, 3)
7676
.tap(_ => upstreamInvocationCounter.incrementAndGet().discard)
77-
.retry(Schedule.immediate.maxRepeats(3))
77+
.retry(Schedule.immediate.maxRetries(3))
7878
.tap { value =>
7979
downstreamInvocationCounter.incrementAndGet().discard
8080
if value == 2 then throw new RuntimeException("downstream failure")
@@ -101,7 +101,7 @@ class FlowOpsRetryTest extends AnyFlatSpec with Matchers with ElapsedTime:
101101

102102
// when/then
103103
val exception = the[ChannelClosedException.Error] thrownBy {
104-
flow.retry(Schedule.immediate.maxRepeats(maxRetries)).runToList()
104+
flow.retry(Schedule.immediate.maxRetries(maxRetries)).runToList()
105105
}
106106

107107
exception.getCause.getMessage shouldBe errorMessage
@@ -122,7 +122,7 @@ class FlowOpsRetryTest extends AnyFlatSpec with Matchers with ElapsedTime:
122122
}
123123

124124
val config = RetryConfig[Throwable, Unit](
125-
Schedule.immediate.maxRepeats(maxRetries),
125+
Schedule.immediate.maxRetries(maxRetries),
126126
ResultPolicy.retryWhen[Throwable, Unit](_.getMessage != fatalErrorMessage)
127127
)
128128

@@ -139,7 +139,7 @@ class FlowOpsRetryTest extends AnyFlatSpec with Matchers with ElapsedTime:
139139
val flow = Flow.empty[Int]
140140

141141
// when
142-
val result = flow.retry(Schedule.immediate.maxRepeats(3)).runToList()
142+
val result = flow.retry(Schedule.immediate.maxRetries(3)).runToList()
143143

144144
// then
145145
result shouldBe List.empty
@@ -153,7 +153,7 @@ class FlowOpsRetryTest extends AnyFlatSpec with Matchers with ElapsedTime:
153153
}
154154

155155
// when
156-
val result = flow.retry(Schedule.immediate.maxRepeats(5)).runToList()
156+
val result = flow.retry(Schedule.immediate.maxRetries(5)).runToList()
157157

158158
// then
159159
result shouldBe List("first try success")
@@ -169,7 +169,7 @@ class FlowOpsRetryTest extends AnyFlatSpec with Matchers with ElapsedTime:
169169
}
170170

171171
// when
172-
val result = flow.retry(Schedule.immediate.maxRepeats(2)).runToList()
172+
val result = flow.retry(Schedule.immediate.maxRetries(2)).runToList()
173173

174174
// then
175175
result shouldBe List(2, 4, 6)
@@ -188,7 +188,7 @@ class FlowOpsRetryTest extends AnyFlatSpec with Matchers with ElapsedTime:
188188
}
189189

190190
// when
191-
val result = flow.retry(Schedule.immediate.maxRepeats(1)).runToList()
191+
val result = flow.retry(Schedule.immediate.maxRetries(1)).runToList()
192192

193193
// then
194194
result shouldBe List(20, 40)
@@ -199,7 +199,7 @@ class FlowOpsRetryTest extends AnyFlatSpec with Matchers with ElapsedTime:
199199
val invocationCounter = new AtomicInteger(0)
200200

201201
val flow =
202-
Flow.fromValues(1 to 10*).tap(_ => invocationCounter.incrementAndGet().discard).take(3).retry(Schedule.immediate.maxRepeats(3))
202+
Flow.fromValues(1 to 10*).tap(_ => invocationCounter.incrementAndGet().discard).take(3).retry(Schedule.immediate.maxRetries(3))
203203

204204
// when
205205
val result = flow.runToList()

core/src/test/scala/ox/resilience/AfterAttemptTest.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class AfterAttemptTest extends AnyFlatSpec with Matchers with EitherValues with
2626
returnedResult = result
2727

2828
// when
29-
val result = retry(RetryConfig(Schedule.immediate.maxRepeats(3), afterAttempt = afterAttempt))(f)
29+
val result = retry(RetryConfig(Schedule.immediate.maxRetries(3), afterAttempt = afterAttempt))(f)
3030

3131
// then
3232
result shouldBe successfulResult
@@ -52,7 +52,7 @@ class AfterAttemptTest extends AnyFlatSpec with Matchers with EitherValues with
5252
returnedResult = result
5353

5454
// when
55-
val result = the[RuntimeException] thrownBy retry(RetryConfig(Schedule.immediate.maxRepeats(3), afterAttempt = afterAttempt))(f)
55+
val result = the[RuntimeException] thrownBy retry(RetryConfig(Schedule.immediate.maxRetries(3), afterAttempt = afterAttempt))(f)
5656

5757
// then
5858
result shouldBe failedResult

core/src/test/scala/ox/resilience/BackoffRetryTest.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class BackoffRetryTest extends AnyFlatSpec with Matchers with EitherValues with
2525

2626
// when
2727
val (result, elapsedTime) =
28-
measure(the[RuntimeException] thrownBy retry(Schedule.exponentialBackoff(initialDelay).maxRepeats(maxRetries))(f))
28+
measure(the[RuntimeException] thrownBy retry(Schedule.exponentialBackoff(initialDelay).maxRetries(maxRetries))(f))
2929

3030
// then
3131
result should have message "boom"
@@ -63,7 +63,7 @@ class BackoffRetryTest extends AnyFlatSpec with Matchers with EitherValues with
6363
// when
6464
val (result, elapsedTime) =
6565
measure(
66-
the[RuntimeException] thrownBy retry(Schedule.exponentialBackoff(initialDelay).maxRepeats(maxRetries).maxInterval(maxDelay))(f)
66+
the[RuntimeException] thrownBy retry(Schedule.exponentialBackoff(initialDelay).maxRetries(maxRetries).maxInterval(maxDelay))(f)
6767
)
6868

6969
// then
@@ -86,7 +86,7 @@ class BackoffRetryTest extends AnyFlatSpec with Matchers with EitherValues with
8686
val (result, elapsedTime) =
8787
measure(
8888
the[RuntimeException] thrownBy retry(
89-
Schedule.exponentialBackoff(initialDelay).maxRepeats(maxRetries).maxInterval(maxDelay).jitter(Jitter.Equal)
89+
Schedule.exponentialBackoff(initialDelay).maxRetries(maxRetries).maxInterval(maxDelay).jitter(Jitter.Equal)
9090
)(f)
9191
)
9292

@@ -108,7 +108,7 @@ class BackoffRetryTest extends AnyFlatSpec with Matchers with EitherValues with
108108
Left(errorMessage)
109109

110110
// when
111-
val (result, elapsedTime) = measure(retryEither(Schedule.exponentialBackoff(initialDelay).maxRepeats(maxRetries))(f))
111+
val (result, elapsedTime) = measure(retryEither(Schedule.exponentialBackoff(initialDelay).maxRetries(maxRetries))(f))
112112

113113
// then
114114
result.left.value shouldBe errorMessage

core/src/test/scala/ox/resilience/FixedIntervalRetryTest.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class DelayedRetryTest extends AnyFlatSpec with Matchers with EitherValues with
2424

2525
// when
2626
val (result, elapsedTime) =
27-
measure(the[RuntimeException] thrownBy retry(Schedule.fixedInterval(sleep).maxRepeats(maxRetries))(f))
27+
measure(the[RuntimeException] thrownBy retry(Schedule.fixedInterval(sleep).maxRetries(maxRetries))(f))
2828

2929
// then
3030
result should have message "boom"
@@ -61,7 +61,7 @@ class DelayedRetryTest extends AnyFlatSpec with Matchers with EitherValues with
6161
Left(errorMessage)
6262

6363
// when
64-
val (result, elapsedTime) = measure(retryEither(Schedule.fixedInterval(sleep).maxRepeats(maxRetries))(f))
64+
val (result, elapsedTime) = measure(retryEither(Schedule.fixedInterval(sleep).maxRetries(maxRetries))(f))
6565

6666
// then
6767
result.left.value shouldBe errorMessage

core/src/test/scala/ox/resilience/ImmediateRetryTest.scala

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class ImmediateRetryTest extends AnyFlatSpec with EitherValues with TryValues wi
2020
successfulResult
2121

2222
// when
23-
val result = retry(Schedule.immediate.maxRepeats(3))(f)
23+
val result = retry(Schedule.immediate.maxRetries(3))(f)
2424

2525
// then
2626
result shouldBe successfulResult
@@ -30,7 +30,7 @@ class ImmediateRetryTest extends AnyFlatSpec with EitherValues with TryValues wi
3030
// given
3131
var counter = 0
3232
val errorMessage = "boom"
33-
val policy = RetryConfig[Throwable, Unit](Schedule.immediate.maxRepeats(3), ResultPolicy.retryWhen(_.getMessage != errorMessage))
33+
val policy = RetryConfig[Throwable, Unit](Schedule.immediate.maxRetries(3), ResultPolicy.retryWhen(_.getMessage != errorMessage))
3434

3535
def f =
3636
counter += 1
@@ -44,7 +44,7 @@ class ImmediateRetryTest extends AnyFlatSpec with EitherValues with TryValues wi
4444
// given
4545
var counter = 0
4646
val unsuccessfulResult = -1
47-
val policy = RetryConfig[Throwable, Int](Schedule.immediate.maxRepeats(3), ResultPolicy.successfulWhen(_ > 0))
47+
val policy = RetryConfig[Throwable, Int](Schedule.immediate.maxRetries(3), ResultPolicy.successfulWhen(_ > 0))
4848

4949
def f =
5050
counter += 1
@@ -67,7 +67,7 @@ class ImmediateRetryTest extends AnyFlatSpec with EitherValues with TryValues wi
6767
if true then throw new RuntimeException(errorMessage)
6868

6969
// when/then
70-
the[RuntimeException] thrownBy retry(Schedule.immediate.maxRepeats(3))(f) should have message errorMessage
70+
the[RuntimeException] thrownBy retry(Schedule.immediate.maxRetries(3))(f) should have message errorMessage
7171
counter shouldBe 4
7272

7373
it should "retry a failing function forever" in:
@@ -97,7 +97,7 @@ class ImmediateRetryTest extends AnyFlatSpec with EitherValues with TryValues wi
9797
Right(successfulResult)
9898

9999
// when
100-
val result = retryEither(Schedule.immediate.maxRepeats(3))(f)
100+
val result = retryEither(Schedule.immediate.maxRetries(3))(f)
101101

102102
// then
103103
result.value shouldBe successfulResult
@@ -107,7 +107,7 @@ class ImmediateRetryTest extends AnyFlatSpec with EitherValues with TryValues wi
107107
// given
108108
var counter = 0
109109
val errorMessage = "boom"
110-
val policy: RetryConfig[String, Int] = RetryConfig(Schedule.immediate.maxRepeats(3), ResultPolicy.retryWhen(_ != errorMessage))
110+
val policy: RetryConfig[String, Int] = RetryConfig(Schedule.immediate.maxRetries(3), ResultPolicy.retryWhen(_ != errorMessage))
111111

112112
def f: Either[String, Int] =
113113
counter += 1
@@ -124,7 +124,7 @@ class ImmediateRetryTest extends AnyFlatSpec with EitherValues with TryValues wi
124124
// given
125125
var counter = 0
126126
val unsuccessfulResult = -1
127-
val policy: RetryConfig[String, Int] = RetryConfig(Schedule.immediate.maxRepeats(3), ResultPolicy.successfulWhen(_ > 0))
127+
val policy: RetryConfig[String, Int] = RetryConfig(Schedule.immediate.maxRetries(3), ResultPolicy.successfulWhen(_ > 0))
128128

129129
def f: Either[String, Int] =
130130
counter += 1
@@ -147,7 +147,7 @@ class ImmediateRetryTest extends AnyFlatSpec with EitherValues with TryValues wi
147147
Left(errorMessage)
148148

149149
// when
150-
val result = retryEither(Schedule.immediate.maxRepeats(3))(f)
150+
val result = retryEither(Schedule.immediate.maxRetries(3))(f)
151151

152152
// then
153153
result.left.value shouldBe errorMessage
@@ -167,7 +167,7 @@ class ImmediateRetryTest extends AnyFlatSpec with EitherValues with TryValues wi
167167

168168
val adaptive = AdaptiveRetry(TokenBucket(5), 1, 1)
169169
// when
170-
val result = adaptive.retryEither(Schedule.immediate.maxRepeats(5))(f)
170+
val result = adaptive.retryEither(Schedule.immediate.maxRetries(5))(f)
171171

172172
// then
173173
result.value shouldBe "Success"
@@ -184,7 +184,7 @@ class ImmediateRetryTest extends AnyFlatSpec with EitherValues with TryValues wi
184184

185185
val adaptive = AdaptiveRetry(TokenBucket(2), 1, 1)
186186
// when
187-
val result = adaptive.retryEither(Schedule.immediate.maxRepeats(5))(f)
187+
val result = adaptive.retryEither(Schedule.immediate.maxRetries(5))(f)
188188

189189
// then
190190
result.left.value shouldBe errorMessage
@@ -202,7 +202,7 @@ class ImmediateRetryTest extends AnyFlatSpec with EitherValues with TryValues wi
202202

203203
val adaptive = AdaptiveRetry(TokenBucket(2), 1, 1)
204204
val retryConfig =
205-
RetryConfig(Schedule.immediate.maxRepeats(5)).copy(resultPolicy = ResultPolicy.successfulWhen[String, String](_ => false))
205+
RetryConfig(Schedule.immediate.maxRetries(5)).copy(resultPolicy = ResultPolicy.successfulWhen[String, String](_ => false))
206206
// when
207207
val result = adaptive.retryEither(retryConfig, _ => false)(f)
208208

core/src/test/scala/ox/resilience/ScheduleFallingBackRetryTest.scala

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ class ScheduleFallingBackRetryTest extends AnyFlatSpec with Matchers with Elapse
2121
counter += 1
2222
throw new RuntimeException("boom")
2323

24-
val schedule = Schedule.immediate.maxRepeats(immediateRetries).andThen(Schedule.fixedInterval(sleep).maxRepeats(delayedRetries))
24+
val schedule =
25+
Schedule.immediate.maxRetries(immediateRetries).andThen(Schedule.fixedInterval(sleep).maxRetries(delayedRetries))
2526

2627
// when
2728
val (result, elapsedTime) = measure(the[RuntimeException] thrownBy retry(RetryConfig(schedule))(f))
@@ -41,7 +42,7 @@ class ScheduleFallingBackRetryTest extends AnyFlatSpec with Matchers with Elapse
4142
counter += 1
4243
if counter <= retriesUntilSuccess then throw new RuntimeException("boom") else successfulResult
4344

44-
val schedule = Schedule.immediate.maxRepeats(100).andThen(Schedule.fixedInterval(2.millis))
45+
val schedule = Schedule.immediate.maxRetries(100).andThen(Schedule.fixedInterval(2.millis))
4546

4647
// when
4748
val result = retry(RetryConfig(schedule))(f)

core/src/test/scala/ox/scheduling/FixedRateRepeatTest.scala

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class FixedRateRepeatTest extends AnyFlatSpec with Matchers with EitherValues wi
1414

1515
it should "repeat a function at fixed rate" in:
1616
// given
17-
val repeats = 3
17+
val attempts = 3
1818
val funcSleepTime = 30.millis
1919
val interval = 100.millis
2020
var counter = 0
@@ -24,17 +24,17 @@ class FixedRateRepeatTest extends AnyFlatSpec with Matchers with EitherValues wi
2424
counter
2525

2626
// when
27-
val (result, elapsedTime) = measure(repeat(Schedule.fixedInterval(interval).maxRepeats(repeats))(f))
27+
val (result, elapsedTime) = measure(repeat(Schedule.fixedInterval(interval).maxAttempts(attempts))(f))
2828

2929
// then
30-
elapsedTime.toMillis should be >= 3 * interval.toMillis + funcSleepTime.toMillis - 5 // tolerance
31-
elapsedTime.toMillis should be < 4 * interval.toMillis
32-
result shouldBe 4
33-
counter shouldBe 4
30+
elapsedTime.toMillis should be >= 2 * interval.toMillis + funcSleepTime.toMillis - 5 // tolerance
31+
elapsedTime.toMillis should be < 3 * interval.toMillis
32+
result shouldBe 3
33+
counter shouldBe 3
3434

3535
it should "repeat a function at fixed rate with initial delay" in:
3636
// given
37-
val repeats = 3
37+
val attempts = 3
3838
val initialDelay = 50.millis
3939
val interval = 100.millis
4040
var counter = 0
@@ -44,13 +44,13 @@ class FixedRateRepeatTest extends AnyFlatSpec with Matchers with EitherValues wi
4444
counter
4545

4646
// when
47-
val (result, elapsedTime) = measure(repeat(Schedule.fixedInterval(interval).maxRepeats(repeats).withInitialDelay(initialDelay))(f))
47+
val (result, elapsedTime) = measure(repeat(Schedule.fixedInterval(interval).maxAttempts(attempts).withInitialDelay(initialDelay))(f))
4848

4949
// then
50-
elapsedTime.toMillis should be >= 3 * interval.toMillis + initialDelay.toMillis - 5 // tolerance
51-
elapsedTime.toMillis should be < 4 * interval.toMillis
52-
result shouldBe 4
53-
counter shouldBe 4
50+
elapsedTime.toMillis should be >= 2 * interval.toMillis + initialDelay.toMillis - 5 // tolerance
51+
elapsedTime.toMillis should be < 3 * interval.toMillis
52+
result shouldBe 3
53+
counter shouldBe 3
5454

5555
it should "repeat a function forever at fixed rate" in:
5656
// given

0 commit comments

Comments
 (0)