I needed a generic way to retry requests that fail. I did not want to litter my model with extra state or clutter my update
function by having to handle additional cases for each request’s code paths.
I found panosoft/elm-cmd-retry but it depends on native code and meets neither of the requirements set above.
Wishful thinking
It should be intuitive to use
decoder
|> Http.get url
|> Retry.retry
|> Task.attempt DataReceived
It should allow us to retry with configuration
decoder
|> Http.get url
|> Retry.retryWith { retries = 3, delay = 10 * Time.second }
|> Task.attempt DataReceived
Implementation
Here’s my best attempt at the Retry
module. It compiles and works as intended in my program.
module Retry exposing (..)
import Http
import Process
import Task exposing (Task)
import Time exposing (Time)
type alias Config =
{ retries : Int
, delay : Time
}
default : Config
default =
{ retries = 5
, delay = 5 * Time.second
}
retry : Http.Request a -> Task Http.Error a
retry =
retryWith default
retryWith : Config -> Http.Request a -> Task Http.Error a
retryWith config req =
let
onError e =
let
_ =
Debug.log ("retrying " ++ (toString req)) e
in
if config.retries == 0 then
Task.fail e
else
Process.sleep config.delay
|> Task.andThen (\_ -> retryWith { config | retries = config.retries - 1 } req)
in
req
|> Http.toTask
|> Task.onError onError
How to use it
As demonstrated in the example below we meet our goals. Our model is not required to keep track of extra state and it’s easy to convert any single-attempt request into a request that retries itself.
module SomeModule exposing (..)
import Retry
import User exposing (User)
...
type alias Model =
{ users : List User }
type Msg
= UsersReceived (Result Http.Error (List User))
init : ( Model, Cmd Msg )
init =
( { users = [] }, getUsers )
getUsers : Cmd Msg
getUsers =
(Decode.list User.decode)
|> Http.get "/users.json"
|> Retry.retry
|> Task.attempt UsersReceived
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
UsersReceived (Ok users) ->
{ model | users = users }
! []
UsersReceived (Err e) ->
let
_ =
Debug.log "request failed" e
in
( model, Cmd.none )
Feedback
I’m new to Elm and I’m asking the community for general feedback on this module.
- Can you see problems I may encounter with it?
- Can you see ways in which it can be improved?
- Am I breaking any Elm conventions?
Possible improvements?
It would be nice to keep the Task
abstraction out of the programmer’s mind entirely. Task.attempt
wraps Http.send
but it’s not nearly as readable.
-- [Task.attempt] leaks Task abstraction
decoder
|> Http.get url
|> Retry.retry
|> Task.attempt DataReceived
-- maybe this would be better?
decoder
|> Http.get url
|> Retry.retry DataReceived
-- or with configuration
decoder
|> Http.get url
|> Retry.retryWith { ... } DataReceived
Or maybe it would be best if we continued with the Http.send
pattern. However, I’m not sure how to make Retry.retry
return the result that Http.send
is expecting.
-- ideal api does not require hard thinking
decoder
|> Http.get url
|> Retry.retryWith { ... }
|> Http.send DataReceived
Revision 2
Thanks to a comment from rasputin303 the API now feels more natural.
-- revision 2 api
decoder
|> Http.get url
|> Retry.send DataReceived
… and with configuration
decoder
|> Http.get url
|> Retry.sendWith { retries = 3, delay = 10 * Time.second } DataReceived
Implementation
Other formatting also increases readability.
module Retry exposing (Config, send, sendWith)
import Http
import Process
import Task exposing (Task)
import Time exposing (Time)
type alias Config =
{ retries : Int
, interval : Time
}
default : Config
default =
{ retries = 5
, interval = 1 * Time.second
}
send : (Result Http.Error a -> msg) -> Http.Request a -> Cmd msg
send =
sendWith default
sendWith : Config -> (Result Http.Error a -> msg) -> Http.Request a -> Cmd msg
sendWith config resultToMessage req =
req
|> Http.toTask
|> Task.onError (retry config req)
|> Task.attempt resultToMessage
retry : Config -> Http.Request a -> Http.Error -> Task Http.Error a
retry config req e =
case config.retries of
0 ->
let
_ =
Debug.log ("retrying " ++ (toString req)) e
in
Task.fail e
_ ->
let
_ =
Debug.log ("retrying " ++ (toString req)) e
wait =
Process.sleep config.interval
next =
req
|> Http.toTask
|> Task.onError (retry { config | retries = config.retries - 1 } req)
in
wait |> Task.andThen (always next)
Revision 3
Discourse did not allow me to add revision 3 to this post. You can see inline with the comments below.