Last active
June 1, 2019 03:04
-
-
Save choonkeat/28b9264c64dd2b0243ab18cdbba4938a to your computer and use it in GitHub Desktop.
Retry a task in Elm. UPDATE: published as https://package.elm-lang.org/packages/choonkeat/elm-retry/latest/
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module Retry exposing (Config, custom, exponentiallyBackoff, simple) | |
{-| Retry a task with default or custom policy. | |
-} | |
import Process | |
import Random | |
import Task exposing (Task) | |
import Time | |
{-| Config to determine our retry policy. `randomizationFactor = Nothing` means constant | |
-} | |
type alias Config = | |
{ seed : Random.Seed | |
, retryInterval : Float | |
, randomizationFactor : Maybe Float | |
, multiplier : Float | |
, maxRetryInterval : Float | |
} | |
exponentialConfig : Config | |
exponentialConfig = | |
{ seed = Random.initialSeed 0 | |
, retryInterval = 500 | |
, randomizationFactor = Just 0.5 | |
, multiplier = 1.5 | |
, maxRetryInterval = 60000 | |
} | |
{-| Based off <https://github.com/cenkalti/backoff/blob/4b4cebaf850ec58f1bb1fec5bdebdf8501c2bc3f/exponential.go#L144-L153> | |
-} | |
nextRetryGenerator : Config -> Random.Generator Float | |
nextRetryGenerator cfg = | |
case cfg.randomizationFactor of | |
Just float -> | |
let | |
minInterval = | |
cfg.retryInterval * float | |
maxInterval = | |
cfg.retryInterval * (1 + float) | |
in | |
Random.float 0 1 | |
|> Random.map (\randf -> cfg.multiplier * (minInterval + (randf * (maxInterval - minInterval + 1)))) | |
Nothing -> | |
Random.constant cfg.retryInterval | |
{-| Exponentially backoff a Task until duration milliseconds | |
Retry.exponentiallyBackoff 20000 doWork | |
|> Task.attempt DidWork | |
-} | |
exponentiallyBackoff : Int -> Task x a -> Task x a | |
exponentiallyBackoff = | |
custom exponentialConfig | |
{-| Retry at constant interval | |
Retry.simple { intervalMillis = 500, attempts = 4 } doWork | |
|> Task.attempt DidWork | |
-} | |
simple : { intervalMillis : Float, attempts : Int } -> Task x a -> Task x a | |
simple cfg = | |
custom | |
{ exponentialConfig | |
| randomizationFactor = Nothing | |
, maxRetryInterval = cfg.intervalMillis | |
, retryInterval = cfg.intervalMillis | |
} | |
(cfg.attempts * round cfg.intervalMillis) | |
{-| Backoff a Task until duration milliseconds, with a custom Config. | |
Try to use `simple` or `exponentiallyBackoff` | |
-} | |
custom : Config -> Int -> Task x a -> Task x a | |
custom cfg duration task = | |
Time.now | |
|> Task.andThen | |
(\now -> | |
let | |
deadline = | |
Time.millisToPosix (duration + Time.posixToMillis now) | |
( nextSeed, _ ) = | |
Random.step | |
Random.independentSeed | |
(Random.initialSeed (Time.posixToMillis now)) | |
nextCfg = | |
{ cfg | seed = nextSeed } | |
in | |
Task.onError (retryUntil deadline task nextCfg) task | |
) | |
retryUntil : Time.Posix -> Task x a -> Config -> x -> Task x a | |
retryUntil deadline task cfg err = | |
Time.now | |
|> Task.andThen | |
(\currNow -> | |
let | |
( nextRetryInterval, nextCfgSeed ) = | |
Random.step (nextRetryGenerator cfg) cfg.seed | |
nextCfg = | |
{ cfg | |
| seed = nextCfgSeed | |
, retryInterval = min cfg.maxRetryInterval nextRetryInterval | |
} | |
in | |
if (Time.posixToMillis currNow + round cfg.retryInterval) > Time.posixToMillis deadline then | |
Task.fail err | |
else | |
Process.sleep cfg.retryInterval | |
|> Task.andThen (\_ -> Task.onError (retryUntil deadline task nextCfg) task) | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment