-
Notifications
You must be signed in to change notification settings - Fork 23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add side effect callbacks during retry #106
Changes from 3 commits
f00486c
f5c899d
766b5d2
d05ffed
efa13ae
c841ce0
0582b73
1219f95
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
package ox.retry | ||
|
||
import ox.retry.RetryLifecycle.* | ||
|
||
/** A case class representing the lifecycle of a retry operation. It contains two optional callbacks: `beforeEachAttempt` and | ||
* `afterEachAttempt`. | ||
* | ||
* @param beforeEachAttempt | ||
* A function that is executed before each retry attempt. It takes the attempt number as a parameter. By default, it's an empty function. | ||
* @param afterEachAttempt | ||
* A function that is executed after each retry attempt. It takes the attempt number and the result of the attempt as parameters. The | ||
* result is represented as an `Either` type, where `Left` represents an error and `Right` represents a successful result. By default, | ||
* it's an empty function. | ||
* @tparam E | ||
* The type of the error in case the retry operation fails. | ||
* @tparam T | ||
* The type of the successful result in case the retry operation succeeds. | ||
*/ | ||
case class RetryLifecycle[E, T]( | ||
beforeEachAttempt: BeforeEachAttempt = _ => (), | ||
afterEachAttempt: AfterEachAttempt[E, T] = (_, _: Either[E, T]) => () | ||
) | ||
|
||
object RetryLifecycle: | ||
type BeforeEachAttempt = Int => Unit | ||
type AfterEachAttempt[E, T] = (Int, Either[E, T]) => Unit | ||
|
||
/** Creates a `RetryLifecycle` instance with default empty callbacks. | ||
* | ||
* @tparam E | ||
* The type of the error in case the retry operation fails. | ||
* @tparam T | ||
* The type of the successful result in case the retry operation succeeds. | ||
* @return | ||
* A `RetryLifecycle` instance with default empty callbacks. | ||
*/ | ||
def default[E, T]: RetryLifecycle[E, T] = RetryLifecycle() | ||
|
||
/** Creates a `RetryLifecycle` instance with a specific `beforeEachAttempt` callback. | ||
* | ||
* @param f | ||
* The function to be used as `beforeEachAttempt`. | ||
* @tparam E | ||
* The type of the error in case the retry operation fails. | ||
* @tparam T | ||
* The type of the successful result in case the retry operation succeeds. | ||
* @return | ||
* A `RetryLifecycle` instance with the specified `beforeEachAttempt`. | ||
*/ | ||
def beforeEachAttempt[E, T](f: BeforeEachAttempt): RetryLifecycle[E, T] = RetryLifecycle(beforeEachAttempt = f) | ||
|
||
/** Creates a `RetryLifecycle` instance with a specific `afterEachAttempt` callback. | ||
* | ||
* @param f | ||
* The function to be used as `afterEachAttempt`. | ||
* @tparam E | ||
* The type of the error in case the retry operation fails. | ||
* @tparam T | ||
* The type of the successful result in case the retry operation succeeds. | ||
* @return | ||
* A `RetryLifecycle` instance with the specified `afterEachAttempt`. | ||
*/ | ||
def afterEachAttempt[E, T](f: AfterEachAttempt[E, T]): RetryLifecycle[E, T] = RetryLifecycle(afterEachAttempt = f) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
package ox.retry | ||
|
||
import org.scalatest.flatspec.AnyFlatSpec | ||
import org.scalatest.matchers.should.Matchers | ||
import org.scalatest.{EitherValues, TryValues} | ||
import ox.retry.* | ||
|
||
class RetryLifecycleTest extends AnyFlatSpec with Matchers with EitherValues with TryValues: | ||
behavior of "Retry lifecycle" | ||
|
||
it should "retry a succeeding function with lifecycle callbacks" in { | ||
// given | ||
var beforeEachAttemptInvoked = false | ||
var beforeEachAttemptInvocationCount = 0 | ||
|
||
var afterEachAttemptInvoked = false | ||
var afterEachAttemptInvocationCount = 0 | ||
|
||
var counter = 0 | ||
val successfulResult = 42 | ||
|
||
def f = | ||
counter += 1 | ||
successfulResult | ||
|
||
def beforeEachAttempt(attempt: Int): Unit = | ||
beforeEachAttemptInvoked = true | ||
beforeEachAttemptInvocationCount += 1 | ||
|
||
var returnedResult: Either[Throwable, Int] = null | ||
def afterEachAttempt(attempt: Int, result: Either[Throwable, Int]): Unit = | ||
afterEachAttemptInvoked = true | ||
afterEachAttemptInvocationCount += 1 | ||
returnedResult = result | ||
|
||
// when | ||
val result = retry(f)( | ||
RetryPolicy.immediate(3), | ||
RetryLifecycle(beforeEachAttempt, afterEachAttempt) | ||
) | ||
|
||
// then | ||
result shouldBe successfulResult | ||
counter shouldBe 1 | ||
|
||
beforeEachAttemptInvoked shouldBe true | ||
beforeEachAttemptInvocationCount shouldBe 1 | ||
|
||
afterEachAttemptInvoked shouldBe true | ||
afterEachAttemptInvocationCount shouldBe 1 | ||
returnedResult shouldBe Right(successfulResult) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Aren't those assertions redundant? If you verify that the callback was invoked once, I guess one of them would be sufficient. Same for the "before" callback. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
} | ||
|
||
it should "retry a failing function with lifecycle callbacks" in { | ||
// given | ||
var beforeEachAttemptInvoked = false | ||
var beforeEachAttemptInvocationCount = 0 | ||
|
||
var afterEachAttemptInvoked = false | ||
var afterEachAttemptInvocationCount = 0 | ||
|
||
var counter = 0 | ||
val failedResult = new RuntimeException("boom") | ||
|
||
def f = | ||
counter += 1 | ||
if true then throw failedResult | ||
|
||
def beforeEachAttempt(attempt: Int): Unit = | ||
beforeEachAttemptInvoked = true | ||
beforeEachAttemptInvocationCount += 1 | ||
|
||
var returnedResult: Either[Throwable, Unit] = null | ||
def afterEachAttempt(attempt: Int, result: Either[Throwable, Unit]): Unit = | ||
afterEachAttemptInvoked = true | ||
afterEachAttemptInvocationCount += 1 | ||
returnedResult = result | ||
|
||
// when | ||
val result = the[RuntimeException] thrownBy retry(f)( | ||
RetryPolicy.immediate(3), | ||
RetryLifecycle(beforeEachAttempt, afterEachAttempt) | ||
) | ||
|
||
// then | ||
result shouldBe failedResult | ||
counter shouldBe 4 | ||
|
||
beforeEachAttemptInvoked shouldBe true | ||
beforeEachAttemptInvocationCount shouldBe 4 | ||
|
||
afterEachAttemptInvoked shouldBe true | ||
afterEachAttemptInvocationCount shouldBe 4 | ||
returnedResult shouldBe Left(failedResult) | ||
DybekK marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering if we could just number the attempts starting from 1 instead of 0 - WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. In my opinion, it is more intuitive to read the code if we get rid of
attempt + 1
in some places.