|
| 1 | +# kotlin-retry |
| 2 | + |
| 3 | +[](https://bintray.com/michaelbull/maven/kotlin-retry/_latestVersion) [](https://travis-ci.org/michaelbull/kotlin-retry) [](https://github.com/michaelbull/kotlin-retry/blob/master/LICENSE) |
| 4 | + |
| 5 | +[`retry`][retry] is a higher-order function for retrying operations that may fail. |
| 6 | + |
| 7 | +```kotlin |
| 8 | +retry(limitAttempts(10) + constantDelay(delayMillis = 50L)) { |
| 9 | + /* your code */ |
| 10 | +} |
| 11 | +``` |
| 12 | + |
| 13 | +## Installation |
| 14 | + |
| 15 | +```groovy |
| 16 | +repositories { |
| 17 | + maven { url = 'https://dl.bintray.com/michaelbull/maven' } |
| 18 | +} |
| 19 | +
|
| 20 | +dependencies { |
| 21 | + compile 'com.michael-bull.kotlin-retry:kotlin-retry:1.0.0' |
| 22 | +} |
| 23 | +``` |
| 24 | + |
| 25 | +## Introduction |
| 26 | + |
| 27 | +IO operations often experience temporary failures that warant re-execution, |
| 28 | +e.g. a database transaction that may fail due to a deadlock.<sup>[[1]][innodb-deadlocks][[2]][postgres-deadlocks]</sup> |
| 29 | + |
| 30 | +> _“even if your application logic is correct, you must still handle the case |
| 31 | +> where a transaction must be retried”_ |
| 32 | +> |
| 33 | +> — _[Deadlocks in InnoDB][innodb-deadlocks]_ |
| 34 | + |
| 35 | +The [`retry`][retry] function simplifies this process by wrapping the |
| 36 | +application logic and applying a specified [`RetryPolicy`][retry-policy]. |
| 37 | + |
| 38 | +In the example below, either of the calls to `customers.nameFromId` may fail, |
| 39 | +abandoning the remaining logic within the `printExchangeBetween` function. As |
| 40 | +such, we may want to retry this operation until 5 attempts in total have been |
| 41 | +executed: |
| 42 | + |
| 43 | +```kotlin |
| 44 | +suspend fun printExchangeBetween(a: Long, b: Long) { |
| 45 | + val customer1 = customers.nameFromId(a) |
| 46 | + val customer2 = customers.nameFromId(b) |
| 47 | + println("$customer1 exchanged with $customer2") |
| 48 | +} |
| 49 | + |
| 50 | +fun main() = runBlocking { |
| 51 | + retry(limitAttempts(5)) { |
| 52 | + printExchangeBetween(1L, 2L) |
| 53 | + } |
| 54 | +} |
| 55 | +``` |
| 56 | + |
| 57 | +We can also provide a [`RetryPolicy`][retry-policy] that only retries failures |
| 58 | +of a specific type. The example below will retry the operation only if the |
| 59 | +reason for failure was a `SQLDataException`, pausing for 20 milliseconds before |
| 60 | +retrying and stopping after 5 total attempts. |
| 61 | + |
| 62 | +```kotlin |
| 63 | +val retryTimeouts: RetryPolicy<Throwable> = { |
| 64 | + if (reason is SQLDataException) RetryImmediately else StopRetrying |
| 65 | +} |
| 66 | + |
| 67 | +suspend fun printExchangeBetween(a: Long, b: Long) { |
| 68 | + val customer1 = customers.nameFromId(a) |
| 69 | + val customer2 = customers.nameFromId(b) |
| 70 | + println("$customer1 exchanged with $customer2") |
| 71 | +} |
| 72 | + |
| 73 | +fun main() = runBlocking { |
| 74 | + retry(retryTimeouts + limitAttempts(5) + constantDelay(20)) { |
| 75 | + printExchangeBetween(1L, 2L) |
| 76 | + } |
| 77 | +} |
| 78 | +``` |
| 79 | + |
| 80 | +## Backoff |
| 81 | + |
| 82 | +The examples above retry executions immediately after they fail, however we may |
| 83 | +wish to spread out retries out with an ever-increasing delay. This is known as |
| 84 | +a "backoff" and comes in many forms. This library includes all the forms of |
| 85 | +backoff strategy detailed the article by Marc Brooker on the AWS Architecture |
| 86 | +Blog entitled ["Exponential Backoff And Jitter"][aws-backoff]. |
| 87 | + |
| 88 | +#### Binary Exponential |
| 89 | + |
| 90 | +> _“exponential backoff means that clients multiply their backoff by a constant |
| 91 | +> after each attempt, up to some maximum value”_ |
| 92 | +> |
| 93 | +> ``` |
| 94 | +> sleep = min(cap, base * 2 ** attempt) |
| 95 | +> ``` |
| 96 | +
|
| 97 | +```kotlin |
| 98 | +retry(limitAttempts(5) + binaryExponentialBackoff(base = 10L, max = 5000L)) { |
| 99 | + /* code */ |
| 100 | +} |
| 101 | +``` |
| 102 | +
|
| 103 | +#### Full Jitter |
| 104 | + |
| 105 | +> _“trying to improve the performance of a system by adding randomness ... we |
| 106 | +> want to spread out the spikes to an approximately constant rate”_ |
| 107 | +> |
| 108 | +> ``` |
| 109 | +> sleep = random_between(0, min(cap, base * 2 ** attempt)) |
| 110 | +> ``` |
| 111 | +
|
| 112 | +```kotlin |
| 113 | +retry(limitAttempts(5) + fullJitterBackoff(base = 10L, max = 5000L)) { |
| 114 | + /* code */ |
| 115 | +} |
| 116 | +``` |
| 117 | +
|
| 118 | +#### Equal Jitter |
| 119 | + |
| 120 | +> _“Equal Jitter, where we always keep some of the backoff and jitter by a |
| 121 | +> smaller amount”_ |
| 122 | +> |
| 123 | +> ``` |
| 124 | +> temp = min(cap, base * 2 ** attempt) |
| 125 | +> sleep = temp / 2 + random_between(0, temp / 2) |
| 126 | +> ``` |
| 127 | +
|
| 128 | +```kotlin |
| 129 | +retry(limitAttempts(5) + equalJitterBackoff(base = 10L, max = 5000L)) { |
| 130 | + /* code */ |
| 131 | +} |
| 132 | +``` |
| 133 | +
|
| 134 | +#### Decorrelated Jitter |
| 135 | + |
| 136 | +> _“Decorrelated Jitter, which is similar to “Full Jitter”, but we also |
| 137 | +> increase the maximum jitter based on the last random value”_ |
| 138 | +> |
| 139 | +> ``` |
| 140 | +> sleep = min(cap, random_between(base, sleep * 3)) |
| 141 | +> ``` |
| 142 | +
|
| 143 | +```kotlin |
| 144 | +retry(limitAttempts(5) + decorrelatedJitterBackoff(base = 10L, max = 5000L)) { |
| 145 | + /* code */ |
| 146 | +} |
| 147 | +``` |
| 148 | +
|
| 149 | +## Inspiration |
| 150 | + |
| 151 | +- [Control.Retry](http://hackage.haskell.org/package/retry-0.8.0.1/docs/Control-Retry.html) |
| 152 | +- [tokio_retry](https://docs.rs/tokio-retry/0.2.0/tokio_retry/) |
| 153 | + |
| 154 | +## Contributing |
| 155 | + |
| 156 | +Bug reports and pull requests are welcome on [GitHub][github]. |
| 157 | + |
| 158 | +## License |
| 159 | + |
| 160 | +This project is available under the terms of the ISC license. See the |
| 161 | +[`LICENSE`](LICENSE) file for the copyright information and licensing terms. |
| 162 | + |
| 163 | +[github]: https://github.com/michaelbull/kotlin-retry |
| 164 | +[retry]: https://github.com/michaelbull/kotlin-retry/blob/master/src/main/kotlin/com/github/michaelbull/retry/Retry.kt |
| 165 | +[innodb-deadlocks]: https://dev.mysql.com/doc/refman/8.0/en/innodb-deadlocks.html |
| 166 | +[postgres-deadlocks]: https://www.postgresql.org/docs/current/explicit-locking.html#LOCKING-DEADLOCKS |
| 167 | +[retry-policy]: https://github.com/michaelbull/kotlin-retry/blob/master/src/main/kotlin/com/github/michaelbull/retry/policy/RetryPolicy.kt |
| 168 | +[aws-backoff]: https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ |
| 169 | +[haskell-retry]: http://hackage.haskell.org/package/retry-0.8.0.1/docs/Control-Retry.html |
0 commit comments