Skip to content

Commit ce70237

Browse files
committed
Merge pull request #36 from jonsterling/ready/33
Add a retry combinator with exponential backoff & optional timeout (issue #33)
2 parents 7ac1f55 + 5091b24 commit ce70237

File tree

4 files changed

+69
-4
lines changed

4 files changed

+69
-4
lines changed

bower.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"purescript-dom": "^0.1.2",
2929
"purescript-foreign": "^0.5.0",
3030
"purescript-integers": "^0.2.0",
31+
"purescript-math": "^0.2.0",
3132
"purescript-nullable": "^0.2.0",
3233
"purescript-unsafe-coerce": "^0.1.0"
3334
},

docs/Network.HTTP.Affjax.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,14 @@ delete_ :: forall e. URL -> Affjax e Unit
144144

145145
Makes a `DELETE` request to the specified URL and ignores the response.
146146

147+
#### `retry`
148+
149+
``` purescript
150+
retry :: forall e a b. (Requestable a) => Maybe Int -> (AffjaxRequest a -> Affjax (avar :: AVAR | e) b) -> AffjaxRequest a -> Affjax (avar :: AVAR | e) b
151+
```
152+
153+
Retry a request with exponential backoff, timing out optionally after a specified number of milliseconds. After the timeout, the last received response is returned; if it was not possible to communicate with the server due to an error, then this is bubbled up.
154+
147155
#### `affjax'`
148156

149157
``` purescript

src/Network/HTTP/Affjax.purs

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,32 @@ module Network.HTTP.Affjax
1010
, post, post_, post', post_'
1111
, put, put_, put', put_'
1212
, delete, delete_
13+
, retry
1314
) where
1415

1516
import Prelude
17+
import Control.Alt ((<|>))
1618
import Control.Bind ((<=<))
17-
import Control.Monad.Aff (Aff(), makeAff, makeAff', Canceler(..))
19+
import Control.Monad.Aff (Aff(), makeAff, makeAff', Canceler(..), attempt, later', forkAff, cancel)
20+
import Control.Monad.Aff.Par (Par(..), runPar)
21+
import Control.Monad.Aff.AVar (AVAR(), makeVar, takeVar, putVar)
1822
import Control.Monad.Eff (Eff())
1923
import Control.Monad.Eff.Exception (Error(), error)
20-
import Data.Either (Either(..))
24+
import Control.Monad.Error.Class (throwError)
25+
import Data.Either (Either(..), either)
2126
import Data.Foreign (Foreign(..), F(), parseJSON, readString)
2227
import Data.Function (Fn5(), runFn5, Fn4(), runFn4)
28+
import Data.Int (toNumber, round)
2329
import Data.Maybe (Maybe(..), maybe)
2430
import Data.Nullable (Nullable(), toNullable)
2531
import DOM.XHR (XMLHttpRequest())
32+
import Math (max, pow)
2633
import Network.HTTP.Affjax.Request
2734
import Network.HTTP.Affjax.Response
2835
import Network.HTTP.Method (Method(..), methodToString)
2936
import Network.HTTP.RequestHeader (RequestHeader(), requestHeaderName, requestHeaderValue)
3037
import Network.HTTP.ResponseHeader (ResponseHeader(), responseHeader)
31-
import Network.HTTP.StatusCode (StatusCode())
38+
import Network.HTTP.StatusCode (StatusCode(..))
3239

3340
-- | The effect type for AJAX requests made with Affjax.
3441
foreign import data AJAX :: !
@@ -117,6 +124,50 @@ delete u = affjax $ defaultRequest { method = DELETE, url = u }
117124
delete_ :: forall e. URL -> Affjax e Unit
118125
delete_ = delete
119126

127+
-- | Either we have a failure (which may be an exception or a failed response), or we have a successful response.
128+
type RetryState e a = Either (Either e a) a
129+
130+
-- | Retry a request with exponential backoff, timing out optionally after a specified number of milliseconds. After the timeout, the last received response is returned; if it was not possible to communicate with the server due to an error, then this is bubbled up.
131+
retry :: forall e a b. (Requestable a) => Maybe Int -> (AffjaxRequest a -> Affjax (avar :: AVAR | e) b) -> (AffjaxRequest a -> Affjax (avar :: AVAR | e) b)
132+
retry milliseconds run req = do
133+
-- failureVar is either an exception or a failed request
134+
failureVar <- makeVar
135+
let loop = go failureVar
136+
case milliseconds of
137+
Nothing -> loop 1
138+
Just milliseconds -> do
139+
respVar <- makeVar
140+
loopHandle <- forkAff $ loop 1 >>= putVar respVar <<< Just
141+
timeoutHandle <-
142+
forkAff <<< later' milliseconds $ do
143+
putVar respVar Nothing
144+
loopHandle `cancel` error "Cancel"
145+
result <- takeVar respVar
146+
case result of
147+
Nothing -> takeVar failureVar >>= either throwError pure
148+
Just resp -> pure resp
149+
where
150+
-- delay at attempt #n with exponential backoff
151+
delay n = round $ max maxDelay $ 100.0 * (pow 2.0 $ toNumber (n - 1))
152+
where
153+
-- maximum delay in milliseconds
154+
maxDelay = 30.0 * 1000.0
155+
156+
retryState :: Either _ _ -> RetryState _ _
157+
retryState (Left exn) = Left $ Left exn
158+
retryState (Right resp) =
159+
case resp.status of
160+
StatusCode 200 -> Right resp
161+
_ -> Left (Right resp)
162+
163+
go failureVar n = do
164+
result <- retryState <$> attempt (run req)
165+
case result of
166+
Left err -> do
167+
putVar failureVar err
168+
later' (delay n) $ go failureVar (n + 1)
169+
Right resp -> pure resp
170+
120171
-- | Run a request directly without using `Aff`.
121172
affjax' :: forall e a b. (Requestable a, Respondable b) =>
122173
AffjaxRequest a ->

test/Main.purs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,19 @@ assertEq x y = if x == y
5151
then return unit
5252
else assertFail $ "Expected " <> show x <> ", got " <> show y
5353

54-
-- | For helping type inference
54+
-- | For helping type inference
5555
typeIs :: forall e a. a -> Assert e Unit
5656
typeIs = const (return unit)
5757

5858
main = runAff throwException (const $ log "affjax: All good!") $ do
5959
let ok200 = StatusCode 200
6060
let notFound404 = StatusCode 404
6161

62+
A.log "GET /does-not-exists: should be 404 Not found after retries"
63+
(attempt $ retry (Just 5000) affjax $ defaultRequest { url = "/does-not-exist" }) >>= assertRight >>= \res -> do
64+
typeIs (res :: AffjaxResponse String)
65+
assertEq notFound404 res.status
66+
6267
A.log "GET /mirror: should be 200 OK"
6368
(attempt $ affjax $ defaultRequest { url = "/mirror" }) >>= assertRight >>= \res -> do
6469
typeIs (res :: AffjaxResponse Foreign)

0 commit comments

Comments
 (0)