packages feed

glue-core-0.7.0: test/Glue/CircuitBreakerSpec.hs

{-# LANGUAGE ScopedTypeVariables #-}

module Glue.CircuitBreakerSpec (spec) where

import           Data.IORef
import           Glue.CircuitBreaker
import           Test.Hspec
import           Test.QuickCheck
import           UnliftIO.Exception  (Exception, throwIO, try)

data CircuitBreakerTestException = CircuitBreakerTestException
  deriving (Eq, Show)

instance Exception CircuitBreakerTestException

requests :: [Int]
requests = [1 .. 10]

spec :: Spec
spec = do
  describe "circuitBreaker" $ do
    it "Multiple failures prevent subsequent calls" $
      property $ \failureMax -> do
        let positiveFailureMax     = abs failureMax `mod` 5
            options                = defaultCircuitBreakerOptions { maxBreakerFailures = positiveFailureMax }
        ref                        <- newIORef (0 :: Int)
        let service _              = atomicModifyIORef' ref (\c -> (c + 1, ())) >> throwIO CircuitBreakerTestException :: IO Int
        (s, circuitBreakerService) <- circuitBreaker options service
        results                    <- traverse (try . try . circuitBreakerService) requests :: IO [Either CircuitBreakerException (Either CircuitBreakerTestException Int)]
        let expectedResults        = replicate (positiveFailureMax + 1) (Right $ Left CircuitBreakerTestException)
                                  ++ replicate (10 - positiveFailureMax - 1) (Left $ CircuitBreakerException "Circuit breaker open.")
        results `shouldBe` expectedResults
        isCircuitBreakerOpen s `shouldReturn` True
        isCircuitBreakerClosed s `shouldReturn` False
        readIORef ref `shouldReturn` (positiveFailureMax + 1)

    it "Successful calls pass straight through" $
      property $ \(failureMax :: Int, requestIDs :: [Int]) -> do
        let positiveFailureMax     = abs failureMax
            options                = defaultCircuitBreakerOptions { maxBreakerFailures = positiveFailureMax }
            service req            = pure $ req + 1
        (s, circuitBreakerService) <- circuitBreaker options service
        results                    <- traverse circuitBreakerService requestIDs
        let expectedResults        = fmap (+ 1) requestIDs
        results `shouldBe` expectedResults
        isCircuitBreakerOpen s `shouldReturn` False
        isCircuitBreakerClosed s `shouldReturn` True

    it "combineCircuitBreakerStates reports open if any breaker is open" $ do
      let opts = defaultCircuitBreakerOptions { maxBreakerFailures = 0 }
      (s1, svc1) <- circuitBreaker opts (\(_ :: Int) -> throwIO CircuitBreakerTestException :: IO Int)
      (s2, _)    <- circuitBreaker opts (\(x :: Int) -> pure (x + 1) :: IO Int)
      let combined = combineCircuitBreakerStates [s1, s2]
      isCircuitBreakerOpen combined `shouldReturn` False
      isCircuitBreakerClosed combined `shouldReturn` True
      _ <- try (svc1 1) :: IO (Either CircuitBreakerTestException Int)
      isCircuitBreakerOpen combined `shouldReturn` True
      isCircuitBreakerClosed combined `shouldReturn` False