Skip to content

Commit 4bc4b4d

Browse files
committed
Merge pull request #41 from jonsterling/ready/40
[Retries] add RetryPolicy (#40)
2 parents e46c526 + d052b57 commit 4bc4b4d

File tree

4 files changed

+74
-23
lines changed

4 files changed

+74
-23
lines changed

bower.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"purescript-integers": "^0.2.0",
3131
"purescript-math": "^0.2.0",
3232
"purescript-nullable": "^0.2.0",
33+
"purescript-refs": "^0.2.0",
3334
"purescript-unsafe-coerce": "^0.1.0"
3435
},
3536
"devDependencies": {

docs/Network.HTTP.Affjax.md

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,13 +144,37 @@ delete_ :: forall e. URL -> Affjax e Unit
144144

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

147+
#### `RetryDelayCurve`
148+
149+
``` purescript
150+
type RetryDelayCurve = Int -> Int
151+
```
152+
153+
A sequence of retry delays, in milliseconds.
154+
155+
#### `RetryPolicy`
156+
157+
``` purescript
158+
type RetryPolicy = { timeout :: Maybe Int, delayCurve :: RetryDelayCurve, shouldRetryWithStatusCode :: StatusCode -> Boolean }
159+
```
160+
161+
Expresses a policy for retrying Affjax requests with backoff.
162+
163+
#### `defaultRetryPolicy`
164+
165+
``` purescript
166+
defaultRetryPolicy :: RetryPolicy
167+
```
168+
169+
A sensible default for retries: no timeout, maximum delay of 30s, initial delay of 0.1s, exponential backoff, and no status code triggers a retry.
170+
147171
#### `retry`
148172

149173
``` 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
174+
retry :: forall e a b. (Requestable a) => RetryPolicy -> (AffjaxRequest a -> Affjax (avar :: AVAR, ref :: REF | e) b) -> AffjaxRequest a -> Affjax (avar :: AVAR, ref :: REF | e) b
151175
```
152176

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.
177+
Retry a request using a `RetryPolicy`. 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.
154178

155179
#### `affjax'`
156180

src/Network/HTTP/Affjax.purs

Lines changed: 45 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ module Network.HTTP.Affjax
1010
, post, post_, post', post_'
1111
, put, put_, put', put_'
1212
, delete, delete_
13+
, RetryDelayCurve()
14+
, RetryPolicy(..)
15+
, defaultRetryPolicy
1316
, retry
1417
) where
1518

@@ -20,7 +23,9 @@ import Control.Monad.Aff (Aff(), makeAff, makeAff', Canceler(..), attempt, later
2023
import Control.Monad.Aff.Par (Par(..), runPar)
2124
import Control.Monad.Aff.AVar (AVAR(), makeVar, takeVar, putVar)
2225
import Control.Monad.Eff (Eff())
26+
import Control.Monad.Eff.Class (liftEff)
2327
import Control.Monad.Eff.Exception (Error(), error)
28+
import Control.Monad.Eff.Ref (REF(), newRef, readRef, writeRef)
2429
import Control.Monad.Error.Class (throwError)
2530
import Data.Either (Either(..), either)
2631
import Data.Foreign (Foreign(..), F(), parseJSON, readString)
@@ -124,48 +129,68 @@ delete u = affjax $ defaultRequest { method = DELETE, url = u }
124129
delete_ :: forall e. URL -> Affjax e Unit
125130
delete_ = delete
126131

132+
-- | A sequence of retry delays, in milliseconds.
133+
type RetryDelayCurve = Int -> Int
134+
135+
-- | Expresses a policy for retrying Affjax requests with backoff.
136+
type RetryPolicy
137+
= { timeout :: Maybe Int -- ^ the timeout in milliseconds, optional
138+
, delayCurve :: RetryDelayCurve
139+
, shouldRetryWithStatusCode :: StatusCode -> Boolean -- ^ whether a non-200 status code should trigger a retry
140+
}
141+
142+
-- | A sensible default for retries: no timeout, maximum delay of 30s, initial delay of 0.1s, exponential backoff, and no status code triggers a retry.
143+
defaultRetryPolicy :: RetryPolicy
144+
defaultRetryPolicy =
145+
{ timeout : Nothing
146+
, delayCurve : \n -> round $ max (30.0 * 1000.0) $ 100.0 * (pow 2.0 $ toNumber (n - 1))
147+
, shouldRetryWithStatusCode : \_ -> false
148+
}
149+
127150
-- | Either we have a failure (which may be an exception or a failed response), or we have a successful response.
128151
type RetryState e a = Either (Either e a) a
129152

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
153+
-- | Retry a request using a `RetryPolicy`. 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+
retry :: forall e a b. (Requestable a) => RetryPolicy -> (AffjaxRequest a -> Affjax (avar :: AVAR, ref :: REF | e) b) -> (AffjaxRequest a -> Affjax (avar :: AVAR, ref :: REF | e) b)
155+
retry policy run req = do
156+
-- failureRef is either an exception or a failed request
157+
failureRef <- liftEff $ newRef Nothing
158+
let loop = go failureRef
159+
case policy.timeout of
137160
Nothing -> loop 1
138-
Just milliseconds -> do
161+
Just timeout -> do
139162
respVar <- makeVar
140163
loopHandle <- forkAff $ loop 1 >>= putVar respVar <<< Just
141164
timeoutHandle <-
142-
forkAff <<< later' milliseconds $ do
165+
forkAff <<< later' timeout $ do
143166
putVar respVar Nothing
144167
loopHandle `cancel` error "Cancel"
145168
result <- takeVar respVar
146169
case result of
147-
Nothing -> takeVar failureVar >>= either throwError pure
170+
Nothing -> do
171+
failure <- liftEff $ readRef failureRef
172+
case failure of
173+
Nothing -> throwError $ error "Timeout"
174+
Just failure -> either throwError pure failure
148175
Just resp -> pure resp
149176
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-
156177
retryState :: Either _ _ -> RetryState _ _
157178
retryState (Left exn) = Left $ Left exn
158179
retryState (Right resp) =
159180
case resp.status of
160181
StatusCode 200 -> Right resp
161-
_ -> Left (Right resp)
182+
code ->
183+
if policy.shouldRetryWithStatusCode code then
184+
Left $ Right resp
185+
else
186+
Right resp
162187

163-
go failureVar n = do
188+
go failureRef n = do
164189
result <- retryState <$> attempt (run req)
165190
case result of
166191
Left err -> do
167-
putVar failureVar err
168-
later' (delay n) $ go failureVar (n + 1)
192+
liftEff $ writeRef failureRef $ Just err
193+
later' (policy.delayCurve n) $ go failureRef (n + 1)
169194
Right resp -> pure resp
170195

171196
-- | Run a request directly without using `Aff`.

test/Main.purs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,10 @@ main = runAff (\e -> print e >>= \_ -> throwException e) (const $ log "affjax: A
6464
let mirror = prefix "/mirror"
6565
let doesNotExist = prefix "/does-not-exist"
6666
let notJson = prefix "/not-json"
67+
let retryPolicy = defaultRetryPolicy { timeout = Just 500, shouldRetryWithStatusCode = \_ -> true }
6768

6869
A.log "GET /does-not-exist: should be 404 Not found after retries"
69-
(attempt $ retry (Just 5000) affjax $ defaultRequest { url = doesNotExist }) >>= assertRight >>= \res -> do
70+
(attempt $ retry retryPolicy affjax $ defaultRequest { url = doesNotExist }) >>= assertRight >>= \res -> do
7071
typeIs (res :: AffjaxResponse String)
7172
assertEq notFound404 res.status
7273

0 commit comments

Comments
 (0)