Skip to content

Commit

Permalink
Add a v3 -> v4 migration guide (#547)
Browse files Browse the repository at this point in the history
Closes #531 

* Add flowcharts to explain the logic for each combinator
* Write a migration guide to help people migrate from v3
* Add a helper method for building an error handler that retries on some
errors
* Use the helper methods where possible in the unit tests
  • Loading branch information
cb372 authored Jan 3, 2025
1 parent ce0a830 commit b4fc737
Show file tree
Hide file tree
Showing 7 changed files with 378 additions and 49 deletions.
14 changes: 14 additions & 0 deletions modules/core/shared/src/main/scala/retry/ResultHandler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,19 @@ object ResultHandler:
): ErrorHandler[F, A] =
(error: Throwable, retryDetails: RetryDetails) => log(error, retryDetails).as(HandlerDecision.Continue)

/** Construct an ErrorHandler that chooses to retry the same action as long as the error is worth retrying.
*
* @param log
* A chance to do logging, increment metrics, etc
*/
def retryOnSomeErrors[F[_]: Functor, A](
isWorthRetrying: Throwable => Boolean,
log: (Throwable, RetryDetails) => F[Unit]
): ErrorHandler[F, A] =
(error: Throwable, retryDetails: RetryDetails) =>
log(error, retryDetails)
.as(if isWorthRetrying(error) then HandlerDecision.Continue else HandlerDecision.Stop)

/** Construct a ValueHandler that chooses to retry the same action until it returns a successful result.
*
* @param log
Expand All @@ -40,3 +53,4 @@ object ResultHandler:
/** Pass this to [[retryOnAllErrors]] or [[retryUntilSuccessful]] if you don't need to do any logging */
def noop[F[_]: Applicative, A]: (A, RetryDetails) => F[Unit] =
(_, _) => Applicative[F].unit
end ResultHandler
39 changes: 7 additions & 32 deletions modules/core/shared/src/test/scala/retry/CombinatorsSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,7 @@ class CombinatorsSuite extends CatsEffectSuite:

// AND a result handler that does no adaptation and treats the 4th result as a success
def mkHandler(fixture: Fixture[String]): ValueHandler[IO, String] =
(result: String, retryDetails: RetryDetails) =>
fixture
.updateState(result, retryDetails)
.as(if result.toInt > 3 then Stop else Continue)
ResultHandler.retryUntilSuccessful(_.toInt > 3, log = fixture.updateState)

// AND an action that returns the attempt count as a string
def mkAction(fixture: Fixture[String]): IO[String] =
Expand Down Expand Up @@ -80,10 +77,7 @@ class CombinatorsSuite extends CatsEffectSuite:

// AND a result handler that does no adaptation and treats the 4th result as a success
def mkHandler(fixture: Fixture[String]): ValueHandler[IO, String] =
(result: String, retryDetails: RetryDetails) =>
fixture
.updateState(result, retryDetails)
.as(if result.toInt > 3 then Stop else Continue)
ResultHandler.retryUntilSuccessful(_.toInt > 3, log = fixture.updateState)

// AND an action that returns the attempt count as a string
def mkAction(fixture: Fixture[String]): IO[String] =
Expand Down Expand Up @@ -166,10 +160,7 @@ class CombinatorsSuite extends CatsEffectSuite:

// AND a result handler that will retry > 10k times
def mkHandler(fixture: Fixture[String]): ValueHandler[IO, String] =
(result: String, retryDetails: RetryDetails) =>
fixture
.updateState(result, retryDetails)
.as(if result.toInt > 20_000 then Stop else Continue)
ResultHandler.retryUntilSuccessful(_.toInt > 20_000, log = fixture.updateState)

// AND an action that returns the attempt count as a string
def mkAction(fixture: Fixture[String]): IO[String] =
Expand Down Expand Up @@ -197,10 +188,7 @@ class CombinatorsSuite extends CatsEffectSuite:

// AND a result handler that retries on all errors
def mkHandler(fixture: Fixture[Throwable]): ErrorHandler[IO, String] =
(error: Throwable, retryDetails: RetryDetails) =>
fixture
.updateState(error, retryDetails)
.as(Continue)
ResultHandler.retryOnAllErrors(log = fixture.updateState)

// AND an action that raises an error twice and then succeeds
def mkAction(fixture: Fixture[Throwable]): IO[String] =
Expand Down Expand Up @@ -238,10 +226,7 @@ class CombinatorsSuite extends CatsEffectSuite:

// AND a result handler that retries on all errors
def mkHandler(fixture: Fixture[Throwable]): ErrorHandler[IO, String] =
(error: Throwable, retryDetails: RetryDetails) =>
fixture
.updateState(error, retryDetails)
.as(Continue)
ResultHandler.retryOnAllErrors(log = fixture.updateState)

// AND an action that always raises an error
def mkAction(fixture: Fixture[Throwable]): IO[String] =
Expand Down Expand Up @@ -276,14 +261,7 @@ class CombinatorsSuite extends CatsEffectSuite:

// AND a result handler that retries on some errors but gives up on others
def mkHandler(fixture: Fixture[Throwable]): ErrorHandler[IO, String] =
(error: Throwable, retryDetails: RetryDetails) =>
fixture
.updateState(error, retryDetails)
.as {
error match
case `oneMoreTimeException` => Continue
case _ => Stop
}
ResultHandler.retryOnSomeErrors(_ == oneMoreTimeException, log = fixture.updateState)

// AND an action that raises a retryable error followed by a non-retryable error
def mkAction(fixture: Fixture[Throwable]): IO[String] =
Expand Down Expand Up @@ -366,10 +344,7 @@ class CombinatorsSuite extends CatsEffectSuite:

// AND a result handler that will always retry
def mkHandler(fixture: Fixture[Throwable]): ErrorHandler[IO, String] =
(error: Throwable, retryDetails: RetryDetails) =>
fixture
.updateState(error, retryDetails)
.as(Continue)
ResultHandler.retryOnAllErrors(log = fixture.updateState)

// AND an action that always raises an error
def mkAction(fixture: Fixture[Throwable]): IO[String] =
Expand Down
10 changes: 2 additions & 8 deletions modules/core/shared/src/test/scala/retry/SyntaxSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,7 @@ class SyntaxSuite extends CatsEffectSuite:

// AND a result handler that does no adaptation and treats the 4th result as a success
def mkHandler(fixture: Fixture[String]): ValueHandler[IO, String] =
(result: String, retryDetails: RetryDetails) =>
fixture
.updateState(result, retryDetails)
.as(if result.toInt > 3 then Stop else Continue)
ResultHandler.retryUntilSuccessful(_.toInt > 3, log = fixture.updateState)

// AND an action that returns the attempt count as a string
def mkAction(fixture: Fixture[String]): IO[String] =
Expand Down Expand Up @@ -80,10 +77,7 @@ class SyntaxSuite extends CatsEffectSuite:

// AND a result handler that retries on all errors
def mkHandler(fixture: Fixture[Throwable]): ErrorHandler[IO, String] =
(error: Throwable, retryDetails: RetryDetails) =>
fixture
.updateState(error, retryDetails)
.as(Continue)
ResultHandler.retryOnAllErrors(log = fixture.updateState)

// AND an action that raises an error twice and then succeeds
def mkAction(fixture: Fixture[Throwable]): IO[String] =
Expand Down
44 changes: 44 additions & 0 deletions modules/docs/src/main/mdoc/docs/combinators.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ The return value is one of:
`Left(...)` to indicate failure
- an error raised in `F`, if either the action or the value handler raised an error

### Semantics

[![](https://mermaid.ink/img/pako:eNp1UstuwjAQ_BXLJ5DgB3KohAptDz1URFya5LCyN8SSY0d-0EZJ_r2ODRREm4N3Z_Y163igTHOkGa2l_mINGEfe96Ui4ctdQIsimmpJ1usnsvtG5h2m-BnMgXEPwqIlaIw2I9nNZlFEMnHV8q6muNQCc0KrKvVA542y5ATS40jeQHGJZnjWynrpSJPwlBqdo_8Nf0zKne5GknvG0NqgLQ4jNuHayzT2ovO2MghwQs2SPrQUrL8q6iL8Q9CGQ-dGEk1x6Dj8bpqSYyTeaGqZ2OTHDq_ihIeg9wWE9AaveuuAkd9rvSnbooR-o3jINv1IIkxJ0X34h_NBV7RF04Lg4RUMM1NS12CLJc2Cy7GGsGxJSzWFVPBO571iNHPG44oa7Y8NzWqQNiAfd90KOBpor2wH6lPrC55-AD8B1nA?type=png)](https://mermaid.live/edit#pako:eNp1UstuwjAQ_BXLJ5DgB3KohAptDz1URFya5LCyN8SSY0d-0EZJ_r2ODRREm4N3Z_Y163igTHOkGa2l_mINGEfe96Ui4ctdQIsimmpJ1usnsvtG5h2m-BnMgXEPwqIlaIw2I9nNZlFEMnHV8q6muNQCc0KrKvVA542y5ATS40jeQHGJZnjWynrpSJPwlBqdo_8Nf0zKne5GknvG0NqgLQ4jNuHayzT2ovO2MghwQs2SPrQUrL8q6iL8Q9CGQ-dGEk1x6Dj8bpqSYyTeaGqZ2OTHDq_ihIeg9wWE9AaveuuAkd9rvSnbooR-o3jINv1IIkxJ0X34h_NBV7RF04Lg4RUMM1NS12CLJc2Cy7GGsGxJSzWFVPBO571iNHPG44oa7Y8NzWqQNiAfd90KOBpor2wH6lPrC55-AD8B1nA)

### Example

For example, let's keep rolling a die until we get a six, using `IO`.

```scala mdoc:silent
Expand Down Expand Up @@ -156,6 +162,12 @@ The return value is either:
- the action repeatedly raised errors and we ran out of retries
- the error handler raised an error

### Semantics

[![](https://mermaid.ink/img/pako:eNptUU1vwjAM_StRTiDBH-hhEhpoO-wwUXFZy8FKXBopTap8sFWU_740BgYaudjP7_nFTk5cWIm84I2236IFF9jHtjYsnTIkNKty2M_ZcvnCNj8oYkDiL2Aixi2G6IxnR9ARR1ZGIdD7WUV15gk3UZNiP3-wqK5WIIKyZk-WoDx6hs5ZN7J3MFKjO71a46MOrCV8Jp8L-6RvM4U0x1Sk2vXu-54y2P6i_U-mK4My01qfVisx3GboM3wywkpCH0aWQ7XrJfytRuLM5BclS6pSnh3e1BF3jyPdsWvUMKyMTK_rhpFlSKKc3n8VX_AOXQdKpj8-TaKahxY7rHmRUokNpFVqXptzkkIMthyM4EVwERfc2XhoedGA9gnFvMlawcFBd6v2YL6sveLzL-oSyzM?type=png)](https://mermaid.live/edit#pako:eNptUU1vwjAM_StRTiDBH-hhEhpoO-wwUXFZy8FKXBopTap8sFWU_740BgYaudjP7_nFTk5cWIm84I2236IFF9jHtjYsnTIkNKty2M_ZcvnCNj8oYkDiL2Aixi2G6IxnR9ARR1ZGIdD7WUV15gk3UZNiP3-wqK5WIIKyZk-WoDx6hs5ZN7J3MFKjO71a46MOrCV8Jp8L-6RvM4U0x1Sk2vXu-54y2P6i_U-mK4My01qfVisx3GboM3wywkpCH0aWQ7XrJfytRuLM5BclS6pSnh3e1BF3jyPdsWvUMKyMTK_rhpFlSKKc3n8VX_AOXQdKpj8-TaKahxY7rHmRUokNpFVqXptzkkIMthyM4EVwERfc2XhoedGA9gnFvMlawcFBd6v2YL6sveLzL-oSyzM)

### Example

For example, let's make a request for a cat gif using our flaky HTTP client,
retrying only if we get an `IOException`.

Expand Down Expand Up @@ -231,6 +243,38 @@ The return value is one of:
- the action repeatedly raised errors and we ran out of retries
- the error handler raised an error

### `ErrorOrValueHandler`

Note that the behaviour of the `ErrorOrValueHandler` is quite subtle.

The interpretation of its decision (a `HandlerDecision`) depends on whether the
action returned a value or raised an error

If the action returned a value, the handler will be given a `Right(someValue)`
to inspect.
* If the handler decides the result of the action was successful, it should
return `Stop`, meaning there is no need to keep retrying. cats-retry will stop
retrying, and return the successful result.
* On the other hand, if the handler decides that action was *not* successful, it
should return `Continue`, meaning the action has not succeeded yet so we should
keep retrying. cats-retry will then consult the retry policy. If the policy
agrees to keep retrying, cats-retry will do so. Otherwise it will return the
failed value.

If the action raised an error, the handler will be given a `Left(someThrowable)`
to inspect.
* If the handler decides the error is worth retrying, it should return
`Continue`. cats-retry will then consult the retry policy. If the policy agrees
to keep retrying, cats-retry will do so. Otherwise it will re-raise the error.
* If the handler decides the error is *not* worth retrying, it should return
`Stop`. cats-retry will re-raise the error.

### Semantics

[![](https://mermaid.ink/img/pako:eNqVU8tugzAQ_BXLp0RKfoBDpahJ20OlVkHpoZCDhZdgydjIj7Qo5N9rbIIgCZHKxcx4ZtfL4BPOJAUc4ZzLn6wgyqD3bSqQe2Lj0Czxy36OlssntPmFzBoI-x1oN5otGKuERkfCLTToq13eiKAc1OlZCm25QUXA5-AeSnyJ2MiqQbHNMtB6loSKSAecWx5q7-cTdtfFMNE2_5ScZfWHeCGMWwV9_8rzU-1XlFSmQX7pvcmuosSNSDLDpNgH61jiv8tVy6C7In2XV3aEnRuz4_oxc4eBjke8518DJ_VKUGdTdYM8nBhoS5gGjUApqRq0aRfXriUDd2nTxZhc4uxmnarxONWh5H-nuDaG38HTE4rbxL3kft437nHefvtR2l4wynpwtBE1yvmhaDpM_zq8cXiBS1AlYdRd1VMrSrEpoIQUR-6VQk7cyClOxdlJiTUyrkWGI6MsLLCS9lDgKCdcO2T9lGtGDoqUPVsR8S3lBZ__AAneb50?type=png)](https://mermaid.live/edit#pako:eNqVU8tugzAQ_BXLp0RKfoBDpahJ20OlVkHpoZCDhZdgydjIj7Qo5N9rbIIgCZHKxcx4ZtfL4BPOJAUc4ZzLn6wgyqD3bSqQe2Lj0Czxy36OlssntPmFzBoI-x1oN5otGKuERkfCLTToq13eiKAc1OlZCm25QUXA5-AeSnyJ2MiqQbHNMtB6loSKSAecWx5q7-cTdtfFMNE2_5ScZfWHeCGMWwV9_8rzU-1XlFSmQX7pvcmuosSNSDLDpNgH61jiv8tVy6C7In2XV3aEnRuz4_oxc4eBjke8518DJ_VKUGdTdYM8nBhoS5gGjUApqRq0aRfXriUDd2nTxZhc4uxmnarxONWh5H-nuDaG38HTE4rbxL3kft437nHefvtR2l4wynpwtBE1yvmhaDpM_zq8cXiBS1AlYdRd1VMrSrEpoIQUR-6VQk7cyClOxdlJiTUyrkWGI6MsLLCS9lDgKCdcO2T9lGtGDoqUPVsR8S3lBZ__AAneb50)

### Example

For example, let's make a request to an API to retrieve details for a record, which we will only retry if:

- A timeout exception occurs
Expand Down
8 changes: 4 additions & 4 deletions modules/docs/src/main/mdoc/docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ println(
(Note: if you're using Scala.js, you'll need a `%%%` instead of `%%`.)

First we'll need a retry policy. We'll keep it simple: retry up to 5 times, with
no delay between attempts. (See the [retry policies page](policies.html) for
no delay between attempts. (See the [retry policies page](./policies) for
information on more powerful policies).

```scala mdoc:silent
Expand Down Expand Up @@ -96,6 +96,6 @@ logMessages.foreach(println)

Next steps:

- Learn about the other available [combinators](combinators.html)
- Learn about the [MTL combinators](mtl-combinators.html)
- Learn more about [retry policies](policies.html)
- Learn about the other available [combinators](./combinators)
- Learn about the [MTL combinators](./mtl-combinators)
- Learn more about [retry policies](./policies)
Loading

0 comments on commit b4fc737

Please sign in to comment.