glue-core-0.8.0.0: test/Glue/FailoverSpec.hs
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Glue.FailoverSpec (spec) where
import qualified Control.Exception as E
import Data.IORef
import Data.List.NonEmpty (NonEmpty (..))
import Glue.Failover
import Test.Hspec
import Test.QuickCheck
import UnliftIO.Exception (Exception, fromException, throwIO, try)
data FailoverTestException = FailoverTestException
deriving (Eq, Show)
instance Exception FailoverTestException
-- | A backend failure, carrying the backend index and whether it should advance.
data BackendException = BackendException Int Bool
deriving (Eq, Show)
instance Exception BackendException
advances :: E.SomeException -> Bool
advances e = maybe True (\(BackendException _ advance) -> advance) (fromException e)
-- | Backends that record their calls, each succeeding or failing (advancing or not) as scripted.
backends :: IORef [Int] -> NonEmpty (Maybe Bool) -> NonEmpty (Int -> IO Int)
backends calls = fmap backend . indexed
where
indexed (x :| xs) = (0, x) :| zip [1 ..] xs
backend (index, script) req = do
atomicModifyIORef' calls (\c -> (c ++ [index], ()))
case script of
Nothing -> pure (req + index)
Just advance -> throwIO (BackendException index advance)
spec :: Spec
spec = do
describe "failover" $ do
it "Failover handles potentially multiple errors" $
property $ \(request, failureMax) ->
let positiveFailureMax = abs failureMax `mod` 10
service req = if req >= 10 then pure (req + 100) else throwIO FailoverTestException
options = defaultFailoverOptions { transformFailoverRequest = (+ 1), maxFailovers = positiveFailureMax }
failOverService = failover options service
successCase = failOverService request `shouldReturn` ((if request <= 10 then 10 else request) + 100)
failureCase = failOverService request `shouldThrow` (== FailoverTestException)
in if request + positiveFailureMax >= 10 then successCase else failureCase
describe "failoverAcross" $ do
it "Tries backends in order and returns the first success with its index" $ do
calls <- newIORef []
let services = backends calls (Just True :| [Just True, Nothing, Nothing])
failoverAcrossIndexed defaultFailoverAcrossOptions services 100 `shouldReturn` (2, 102)
readIORef calls `shouldReturn` [0, 1, 2]
it "Stops at a failure that does not advance" $ do
calls <- newIORef []
let services = backends calls (Just True :| [Just False, Nothing])
options = defaultFailoverAcrossOptions { advanceOn = advances }
failoverAcross options services 1 `shouldThrow` (== BackendException 1 False)
readIORef calls `shouldReturn` [0, 1]
it "Reports the last or first failure when every backend fails" $ do
calls <- newIORef []
let services = backends calls (Just True :| [Just True, Just True])
failoverAcross defaultFailoverAcrossOptions services 1 `shouldThrow` (== BackendException 2 True)
failoverAcross defaultFailoverAcrossOptions { reportedFailure = ReportFirstFailure } services 1
`shouldThrow` (== BackendException 0 True)
it "Propagates an asynchronous exception without advancing" $ do
calls <- newIORef ([] :: [Int])
let interrupted _ = atomicModifyIORef' calls (\c -> (c ++ [0], ())) >> E.throwIO E.UserInterrupt :: IO Int
services = interrupted :| [\req -> atomicModifyIORef' calls (\c -> (c ++ [1], req))]
failoverAcross defaultFailoverAcrossOptions services 1 `shouldThrow` (== E.UserInterrupt)
readIORef calls `shouldReturn` [0]
it "Behaves like the Either shape for the same outcomes" $
property $ \(script :: [Maybe Bool], first :: Bool) -> do
calls <- newIORef []
let scripted = Just True :| script
options :: FailoverAcrossOptions e
options = defaultFailoverAcrossOptions { reportedFailure = if first then ReportFirstFailure else ReportLastFailure }
valued = fmap (\(index, s) req -> pure (maybe (Right (req + index)) (\advance -> Left (index, advance)) s :: Either (Int, Bool) Int))
((0, Just True) :| zip [1 ..] script)
viaException <- try (failoverAcrossIndexed options { advanceOn = advances } (backends calls scripted) 10)
viaValue <- failoverAcrossIndexedEither options { advanceOn = snd } valued 10
either (\(BackendException i a) -> Left (i, a)) Right viaException `shouldBe` viaValue