Skip to content

Instantly share code, notes, and snippets.

@choonkeat choonkeat/Retry.elm
Last active Jun 1, 2019

Embed
What would you like to do?
Retry a task in Elm. UPDATE: published as https://package.elm-lang.org/packages/choonkeat/elm-retry/latest/
module Retry exposing (ErrTask(..), constantInterval, exponentialBackoff, maxDuration, maxRetries, with)
{-| Retry a task with list of retry policies
config =
[ Retry.maxDuration 7000
, Retry.exponentialBackoff { interval = 500, maxInterval = 3000 }
]
doTask
|> Retry.with config
|> Task.attempt DidTask
-}
import Process
import Random
import Task exposing (Task)
import Time exposing (now)
{-| Specifies a task to run that will return the new task to run on the next error.
The arguments are
- `Int` when we originally started our rawTask, in milliseconds
- `ErrTask x` is the current `ErrTask`; destructure to obtain the function to call
- `x` refers to the latest error from the rawTask
-}
type ErrTask x
= ErrTask (Int -> ErrTask x -> x -> Task x (ErrTask x))
{-| given a List of `ErrTask` we add retries to our rawTask
-}
with : List (ErrTask x) -> Task x a -> Task x a
with errTasks rawTask =
let
onError startTime currErrTasks err =
currErrTasks
|> List.map (\((ErrTask nextErrTask) as cfg) -> nextErrTask startTime cfg err)
|> Task.sequence
|> Task.andThen (\nextErrTasks -> Task.onError (onError startTime nextErrTasks) rawTask)
in
Task.map Time.posixToMillis Time.now
|> Task.andThen
(\nowMillis -> Task.onError (onError nowMillis errTasks) rawTask)
--
--
-- Some out-of-the-box `ErrTask x` to use; combine them into a List for `with`
--
--
{-| limit by "number of retries"
Retry.with [ Retry.maxRetries 20 ] doWork
|> Task.attempt DidWork
NOTE: the code above does NOT sleep between retries; see `constantInterval` or `exponentialBackoff`
-}
maxRetries : Int -> ErrTask x
maxRetries int =
let
nextErrTask _ _ err =
if int <= 0 then
Task.fail err
else
Task.succeed (maxRetries (int - 1))
in
ErrTask nextErrTask
{-| limit by "how long it took"
Retry.with [ Retry.maxDuration 7000 ] doWork
|> Task.attempt DidWork
NOTE: the code above does NOT sleep between retries; see `constantInterval` or `exponentialBackoff`
-}
maxDuration : Int -> ErrTask x
maxDuration duration =
let
nextErrTask startTime sameTask err =
Task.map Time.posixToMillis Time.now
|> Task.andThen
(\now ->
if now - startTime >= duration then
Task.fail err
else
Task.succeed sameTask
)
in
ErrTask nextErrTask
{-| sleep at constant interval between retries
Retry.with [ Retry.constantInterval 1000 ] doWork
|> Task.attempt DidWork
NOTE: the code above will keep retrying rawTask; see `maxRetries` or `maxDuration`
-}
constantInterval : Float -> ErrTask x
constantInterval duration =
let
nextErrTask _ sameTask _ =
Process.sleep duration
|> Task.andThen (\_ -> Task.succeed sameTask)
in
ErrTask nextErrTask
{-| sleep longer and longer between retries
Retry.with [ Retry.exponentialBackoff { interval = 500, maxInterval = 3000 } ] doWork
|> Task.attempt DidWork
NOTE: the code above will keep retrying rawTask; see `maxRetries` or `maxDuration`
-}
exponentialBackoff : { interval : Float, maxInterval : Float } -> ErrTask x
exponentialBackoff { interval, maxInterval } =
let
backoffWith seed currInterval =
let
( calcInterval, nextSeed ) =
Random.step
(nextIntervalGenerator { randomizationFactor = 0.5, multiplier = 1.5, interval = currInterval })
seed
nextErrTask _ _ err =
Process.sleep currInterval
|> Task.andThen (\_ -> Task.succeed (backoffWith nextSeed (min calcInterval maxInterval)))
in
ErrTask nextErrTask
in
backoffWith (Random.initialSeed 0) interval
{-| Based off <https://github.com/cenkalti/backoff/blob/4b4cebaf850ec58f1bb1fec5bdebdf8501c2bc3f/exponential.go#L144-L153>
-}
nextIntervalGenerator : { randomizationFactor : Float, multiplier : Float, interval : Float } -> Random.Generator Float
nextIntervalGenerator { randomizationFactor, multiplier, interval } =
let
minInterval =
interval * randomizationFactor
maxInterval =
interval * (1 + randomizationFactor)
in
Random.float 0 1
|> Random.map (\randf -> multiplier * (minInterval + (randf * (maxInterval - minInterval + 1))))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.