Skip to content

Commit caab433

Browse files
committed
[Retries] add RetryPolicy (close #40)
1 parent e46c526 commit caab433

File tree

3 files changed

+52
-17
lines changed

3 files changed

+52
-17
lines changed

docs/Network.HTTP.Affjax.md

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

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

147+
#### `RetryPolicy`
148+
149+
``` purescript
150+
type RetryPolicy = { timeout :: Maybe Int, delayCurve :: RetryDelayCurve, shouldRetryWithStatusCode :: StatusCode -> Boolean }
151+
```
152+
153+
Expresses a policy for retrying Affjax requests with backoff.
154+
155+
#### `defaultRetryPolicy`
156+
157+
``` purescript
158+
defaultRetryPolicy :: RetryPolicy
159+
```
160+
161+
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.
162+
147163
#### `retry`
148164

149165
``` 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
166+
retry :: forall e a b. (Requestable a) => RetryPolicy -> (AffjaxRequest a -> Affjax (avar :: AVAR | e) b) -> AffjaxRequest a -> Affjax (avar :: AVAR | e) b
151167
```
152168

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.
169+
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.
154170

155171
#### `affjax'`
156172

src/Network/HTTP/Affjax.purs

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ module Network.HTTP.Affjax
1010
, post, post_, post', post_'
1111
, put, put_, put', put_'
1212
, delete, delete_
13+
, RetryPolicy(..)
14+
, defaultRetryPolicy
1315
, retry
1416
) where
1517

@@ -124,48 +126,64 @@ delete u = affjax $ defaultRequest { method = DELETE, url = u }
124126
delete_ :: forall e. URL -> Affjax e Unit
125127
delete_ = delete
126128

129+
-- | A sequence of retry delays, in milliseconds.
130+
type RetryDelayCurve = Int -> Int
131+
132+
-- | Expresses a policy for retrying Affjax requests with backoff.
133+
type RetryPolicy
134+
= { timeout :: Maybe Int -- ^ the timeout in milliseconds, optional
135+
, delayCurve :: RetryDelayCurve
136+
, shouldRetryWithStatusCode :: StatusCode -> Boolean -- ^ whether a non-200 status code should trigger a retry
137+
}
138+
139+
-- | 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.
140+
defaultRetryPolicy :: RetryPolicy
141+
defaultRetryPolicy =
142+
{ timeout : Nothing
143+
, delayCurve : \n -> round $ max (30.0 * 1000.0) $ 100.0 * (pow 2.0 $ toNumber (n - 1))
144+
, shouldRetryWithStatusCode : \_ -> false
145+
}
146+
127147
-- | Either we have a failure (which may be an exception or a failed response), or we have a successful response.
128148
type RetryState e a = Either (Either e a) a
129149

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
150+
-- | 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.
151+
retry :: forall e a b. (Requestable a) => RetryPolicy -> (AffjaxRequest a -> Affjax (avar :: AVAR | e) b) -> (AffjaxRequest a -> Affjax (avar :: AVAR | e) b)
152+
retry policy run req = do
133153
-- failureVar is either an exception or a failed request
134154
failureVar <- makeVar
135155
let loop = go failureVar
136-
case milliseconds of
156+
case policy.timeout of
137157
Nothing -> loop 1
138-
Just milliseconds -> do
158+
Just timeout -> do
139159
respVar <- makeVar
140160
loopHandle <- forkAff $ loop 1 >>= putVar respVar <<< Just
141161
timeoutHandle <-
142-
forkAff <<< later' milliseconds $ do
162+
forkAff <<< later' timeout $ do
143163
putVar respVar Nothing
144164
loopHandle `cancel` error "Cancel"
145165
result <- takeVar respVar
146166
case result of
147167
Nothing -> takeVar failureVar >>= either throwError pure
148168
Just resp -> pure resp
149169
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-
156170
retryState :: Either _ _ -> RetryState _ _
157171
retryState (Left exn) = Left $ Left exn
158172
retryState (Right resp) =
159173
case resp.status of
160174
StatusCode 200 -> Right resp
161-
_ -> Left (Right resp)
175+
code ->
176+
if policy.shouldRetryWithStatusCode code then
177+
Left $ Right resp
178+
else
179+
Right resp
162180

163181
go failureVar n = do
164182
result <- retryState <$> attempt (run req)
165183
case result of
166184
Left err -> do
167185
putVar failureVar err
168-
later' (delay n) $ go failureVar (n + 1)
186+
later' (policy.delayCurve n) $ go failureVar (n + 1)
169187
Right resp -> pure resp
170188

171189
-- | 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)