Skip to content

Instantly share code, notes, and snippets.

@choonkeat
Last active June 1, 2019 03:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save choonkeat/28b9264c64dd2b0243ab18cdbba4938a to your computer and use it in GitHub Desktop.
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/
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