tower-hs-0.2.0.0: test/Tower/Middleware/RetrySpec.hs
{-# LANGUAGE OverloadedStrings #-}
module Tower.Middleware.RetrySpec (spec) where
import Data.IORef
import Data.Time.Clock (NominalDiffTime)
import Test.Hspec
import Test.QuickCheck
import Tower.Service
import Tower.Error
import Tower.Error.Testing ()
import Data.Function ((&))
import Tower.Middleware.Retry
spec :: Spec
spec = describe "Retry middleware" $ do
describe "withRetry" $ do
it "returns success immediately without retrying" $ do
callCount <- newIORef (0 :: Int)
let svc :: Service () String
svc = Service $ \_ -> do
modifyIORef' callCount (+ 1)
pure (Right "ok")
retried = svc & withRetry (constantBackoff 3 0)
result <- runService retried ()
result `shouldBe` Right "ok"
readIORef callCount >>= (`shouldBe` 1)
it "retries on failure up to max retries" $ do
callCount <- newIORef (0 :: Int)
let svc :: Service () String
svc = Service $ \_ -> do
modifyIORef' callCount (+ 1)
pure (Left (CustomError "fail"))
retried = svc & withRetry (constantBackoff 3 0)
result <- runService retried ()
case result of
Left (RetryExhausted n _) -> n `shouldBe` 3
other -> expectationFailure $ "Expected RetryExhausted, got: " ++ show other
readIORef callCount >>= (`shouldBe` 4) -- 1 initial + 3 retries
it "succeeds after transient failures" $ do
callCount <- newIORef (0 :: Int)
let svc :: Service () String
svc = Service $ \_ -> do
n <- readIORef callCount
modifyIORef' callCount (+ 1)
if n < 2
then pure (Left (CustomError "transient"))
else pure (Right "recovered")
retried = svc & withRetry (constantBackoff 3 0)
result <- runService retried ()
result `shouldBe` Right "recovered"
readIORef callCount >>= (`shouldBe` 3) -- 2 failures + 1 success
it "retries zero times when maxRetries is 0" $ do
callCount <- newIORef (0 :: Int)
let svc :: Service () String
svc = Service $ \_ -> do
modifyIORef' callCount (+ 1)
pure (Left (CustomError "fail"))
retried = svc & withRetry (constantBackoff 0 0)
result <- runService retried ()
case result of
Left (RetryExhausted 0 _) -> pure ()
other -> expectationFailure $ "Expected RetryExhausted 0, got: " ++ show other
readIORef callCount >>= (`shouldBe` 1)
describe "computeDelay" $ do
it "constant backoff returns same delay regardless of attempt" $ do
let strategy = constantBackoff 3 2.0
computeDelay strategy 0 `shouldBe` 2.0
computeDelay strategy 1 `shouldBe` 2.0
computeDelay strategy 5 `shouldBe` 2.0
it "exponential backoff grows by multiplier" $ do
let strategy = exponentialBackoff 5 1.0 2.0
computeDelay strategy 0 `shouldBe` 1.0
computeDelay strategy 1 `shouldBe` 2.0
computeDelay strategy 2 `shouldBe` 4.0
computeDelay strategy 3 `shouldBe` 8.0
it "exponential delay is always positive" $ property $
\(Positive base) (Positive mult) (NonNegative attempt) ->
let strategy = exponentialBackoff 10 (realToFrac (base :: Double)) (mult :: Double)
in computeDelay strategy (attempt :: Int) >= (0 :: NominalDiffTime)