{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE DeriveDataTypeable #-} -- | Module containing circuit breaker functionality, which is the ability to open a circuit once a number of failures have occurred, thereby preventing later calls from attempting to make unsuccessful calls. -- | Often this is useful if the underlying service were to repeatedly time out, so as to reduce the number of calls inflight holding up upstream callers. module Glue.CircuitBreaker( CircuitBreakerOptions , CircuitBreakerStatus , CircuitBreakerException(..) , defaultCircuitBreakerOptions , circuitBreaker , maxBreakerFailures , resetTimeoutSecs , breakerDescription ) where import Control.Exception.Lifted import Control.Monad.Base import Control.Monad.Trans.Control import Data.IORef.Lifted import Data.Time.Clock.POSIX import Data.Typeable import Glue.Types -- | Options for determining behaviour of circuit breaking services. data CircuitBreakerOptions = CircuitBreakerOptions { maxBreakerFailures :: Int -- ^ How many times the underlying service must fail in the given window before the circuit opens. , resetTimeoutSecs :: Int -- ^ The window of time in which the underlying service must fail for the circuit to open. , breakerDescription :: String -- ^ Description that is attached to the failure so as to identify the particular circuit. } -- | Defaulted options for the circuit breaker with 3 failures over 60 seconds. defaultCircuitBreakerOptions :: CircuitBreakerOptions defaultCircuitBreakerOptions = CircuitBreakerOptions { maxBreakerFailures = 3, resetTimeoutSecs = 60, breakerDescription = "Circuit breaker open." } -- | Status indicating if the circuit is open. data CircuitBreakerStatus = CircuitBreakerClosed Int | CircuitBreakerOpen Int -- | Exception thrown when the circuit is open. data CircuitBreakerException = CircuitBreakerException String deriving (Eq, Show, Typeable) instance Exception CircuitBreakerException -- TODO: Check that values within m aren't lost on a successful call. -- | Circuit breaking services can be constructed with this function. circuitBreaker :: (MonadBaseControl IO m, MonadBaseControl IO n) => CircuitBreakerOptions -- ^ Options for specifying the circuit breaker behaviour. -> BasicService m a b -- ^ Service to protect with the circuit breaker. -> n (IORef CircuitBreakerStatus, BasicService m a b) circuitBreaker options service = let getCurrentTime = liftBase $ round `fmap` getPOSIXTime failureMax = maxBreakerFailures options callIfClosed request ref = bracketOnError (return ()) (\_ -> incErrors ref) (\_ -> service request) canaryCall request ref = do result <- callIfClosed request ref writeIORef ref $ CircuitBreakerClosed 0 return result incErrors ref = do currentTime <- getCurrentTime atomicModifyIORef' ref $ \status -> case status of (CircuitBreakerClosed errorCount) -> (if errorCount >= failureMax then CircuitBreakerOpen (currentTime + (resetTimeoutSecs options)) else CircuitBreakerClosed (errorCount + 1), ()) other -> (other, ()) failingCall = throw $ CircuitBreakerException $ breakerDescription options callIfOpen request ref = do currentTime <- getCurrentTime canaryRequest <- atomicModifyIORef' ref $ \status -> case status of (CircuitBreakerClosed _) -> (status, False) (CircuitBreakerOpen time) -> if currentTime > time then ((CircuitBreakerOpen (currentTime + (resetTimeoutSecs options))), True) else (status, False) if canaryRequest then canaryCall request ref else failingCall breakerService ref request = do status <- readIORef ref case status of (CircuitBreakerClosed _) -> callIfClosed request ref (CircuitBreakerOpen _) -> callIfOpen request ref in do ref <- newIORef $ CircuitBreakerClosed 0 return (ref, breakerService ref)