module Ecluse.Core.Rules.Effectful (
Resilience (..),
EffectfulConfig (..),
defaultEffectfulConfig,
newBreaker,
FaultReporter (..),
reportFault,
runResilient,
backoffPolicy,
) where
import Control.Retry (
RetryPolicyM (RetryPolicyM),
RetryStatus (rsIterNumber),
retrying,
)
import Data.Time (NominalDiffTime, UTCTime)
import UnliftIO (timeout, tryAny)
import Ecluse.Core.Breaker (
Breaker,
BreakerReporter,
admit,
initialBreaker,
recordFailure,
recordSuccess,
reportBreakerChange,
)
import Ecluse.Core.Package (PackageDetails)
import Ecluse.Core.Rules.Types
import Ecluse.Core.Text (displayExceptionT)
data Resilience = Resilience
{ Resilience -> EffectfulConfig
resConfig :: EffectfulConfig
, Resilience -> FailureAlignment
resAlignment :: FailureAlignment
, Resilience -> TVar Breaker
resBreaker :: TVar Breaker
, Resilience -> BreakerReporter
resBreakerReporter :: BreakerReporter
, Resilience -> IO UTCTime
resClock :: IO UTCTime
, Resilience -> FaultReporter
resFaultReporter :: FaultReporter
}
newtype FaultReporter = FaultReporter (Text -> Text -> IO ())
reportFault :: FaultReporter -> Text -> Text -> IO ()
reportFault :: FaultReporter -> Reason -> Reason -> IO ()
reportFault (FaultReporter Reason -> Reason -> IO ()
report) = Reason -> Reason -> IO ()
report
runResilient :: Resilience -> Text -> (PackageDetails -> IO RuleVerdict) -> PackageDetails -> IO RuleEvaluation
runResilient :: Resilience
-> Reason
-> (PackageDetails -> IO RuleVerdict)
-> PackageDetails
-> IO RuleEvaluation
runResilient Resilience
res Reason
name PackageDetails -> IO RuleVerdict
evalAt PackageDetails
pd = do
admitted <- Resilience -> UTCTime -> IO Bool
admitProbe Resilience
res (UTCTime -> IO Bool) -> IO UTCTime -> IO Bool
forall (m :: * -> *) a b. Monad m => (a -> m b) -> m a -> m b
=<< Resilience -> IO UTCTime
resClock Resilience
res
if not admitted
then
pure (exhausted res name (transientCause (resConfig res)) "the rule source circuit breaker is open")
else do
result <- attemptWithRetry res evalAt pd
settledNow <- resClock res
settleOutcome res name settledNow result
settleOutcome :: Resilience -> Text -> UTCTime -> Either (Transience, Text) RuleVerdict -> IO RuleEvaluation
settleOutcome :: Resilience
-> Reason
-> UTCTime
-> Either (Transience, Reason) RuleVerdict
-> IO RuleEvaluation
settleOutcome Resilience
res Reason
name UTCTime
now = \case
Right RuleVerdict
verdict -> do
Resilience -> (Breaker -> Breaker) -> IO ()
commitBreaker Resilience
res Breaker -> Breaker
recordSuccess
RuleEvaluation -> IO RuleEvaluation
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (RuleVerdict -> RuleEvaluation
Decided RuleVerdict
verdict)
Left (Transience
transience, Reason
detail) -> do
Resilience -> (Breaker -> Breaker) -> IO ()
commitBreaker Resilience
res (EffectfulConfig -> UTCTime -> Breaker -> Breaker
tripOnFailure (Resilience -> EffectfulConfig
resConfig Resilience
res) UTCTime
now)
FaultReporter -> Reason -> Reason -> IO ()
reportFault (Resilience -> FaultReporter
resFaultReporter Resilience
res) Reason
name Reason
detail
RuleEvaluation -> IO RuleEvaluation
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (Resilience -> Reason -> Transience -> Reason -> RuleEvaluation
exhausted Resilience
res Reason
name Transience
transience Reason
"the rule could not be evaluated")
attemptWithRetry :: Resilience -> (PackageDetails -> IO RuleVerdict) -> PackageDetails -> IO (Either (Transience, Text) RuleVerdict)
attemptWithRetry :: Resilience
-> (PackageDetails -> IO RuleVerdict)
-> PackageDetails
-> IO (Either (Transience, Reason) RuleVerdict)
attemptWithRetry Resilience
res PackageDetails -> IO RuleVerdict
evalAt PackageDetails
pd =
RetryPolicyM IO
-> (RetryStatus
-> Either (Transience, Reason) RuleVerdict -> IO Bool)
-> (RetryStatus -> IO (Either (Transience, Reason) RuleVerdict))
-> IO (Either (Transience, Reason) RuleVerdict)
forall (m :: * -> *) b.
MonadIO m =>
RetryPolicyM m
-> (RetryStatus -> b -> m Bool) -> (RetryStatus -> m b) -> m b
retrying ([Int] -> RetryPolicyM IO
backoffPolicy (EffectfulConfig -> [Int]
ecBackoff (Resilience -> EffectfulConfig
resConfig Resilience
res))) RetryStatus -> Either (Transience, Reason) RuleVerdict -> IO Bool
forall {f :: * -> *} {p} {a} {b}.
Applicative f =>
p -> Either a b -> f Bool
shouldRetry (\RetryStatus
_ -> Resilience
-> (PackageDetails -> IO RuleVerdict)
-> PackageDetails
-> IO (Either (Transience, Reason) RuleVerdict)
attemptOnce Resilience
res PackageDetails -> IO RuleVerdict
evalAt PackageDetails
pd)
where
shouldRetry :: p -> Either a b -> f Bool
shouldRetry p
_ = Bool -> f Bool
forall a. a -> f a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (Bool -> f Bool) -> (Either a b -> Bool) -> Either a b -> f Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Either a b -> Bool
forall a b. Either a b -> Bool
isLeft
backoffPolicy :: [Int] -> RetryPolicyM IO
backoffPolicy :: [Int] -> RetryPolicyM IO
backoffPolicy [Int]
backoffs = (RetryStatus -> IO (Maybe Int)) -> RetryPolicyM IO
forall (m :: * -> *).
(RetryStatus -> m (Maybe Int)) -> RetryPolicyM m
RetryPolicyM (\RetryStatus
rs -> Maybe Int -> IO (Maybe Int)
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure ([Int]
backoffs [Int] -> Int -> Maybe Int
forall a. [a] -> Int -> Maybe a
!!? RetryStatus -> Int
rsIterNumber RetryStatus
rs))
attemptOnce :: Resilience -> (PackageDetails -> IO RuleVerdict) -> PackageDetails -> IO (Either (Transience, Text) RuleVerdict)
attemptOnce :: Resilience
-> (PackageDetails -> IO RuleVerdict)
-> PackageDetails
-> IO (Either (Transience, Reason) RuleVerdict)
attemptOnce Resilience
res PackageDetails -> IO RuleVerdict
evalAt PackageDetails
pd = do
result <- IO (Maybe RuleVerdict)
-> IO (Either SomeException (Maybe RuleVerdict))
forall (m :: * -> *) a.
MonadUnliftIO m =>
m a -> m (Either SomeException a)
tryAny (Int -> IO RuleVerdict -> IO (Maybe RuleVerdict)
forall (m :: * -> *) a.
MonadUnliftIO m =>
Int -> m a -> m (Maybe a)
timeout (EffectfulConfig -> Int
ecTimeout (Resilience -> EffectfulConfig
resConfig Resilience
res)) (PackageDetails -> IO RuleVerdict
evalAt PackageDetails
pd))
pure $ case result of
Left SomeException
e -> (Transience, Reason) -> Either (Transience, Reason) RuleVerdict
forall a b. a -> Either a b
Left (Transience
transient, Reason
"the rule threw: " Reason -> Reason -> Reason
forall a. Semigroup a => a -> a -> a
<> SomeException -> Reason
forall e. Exception e => e -> Reason
displayExceptionT SomeException
e)
Right Maybe RuleVerdict
Nothing -> (Transience, Reason) -> Either (Transience, Reason) RuleVerdict
forall a b. a -> Either a b
Left (Transience
transient, Reason
"the attempt timed out")
Right (Just RuleVerdict
verdict) -> RuleVerdict -> Either (Transience, Reason) RuleVerdict
forall a b. b -> Either a b
Right RuleVerdict
verdict
where
transient :: Transience
transient = EffectfulConfig -> Transience
transientCause (Resilience -> EffectfulConfig
resConfig Resilience
res)
exhausted :: Resilience -> Text -> Transience -> Text -> RuleEvaluation
exhausted :: Resilience -> Reason -> Transience -> Reason -> RuleEvaluation
exhausted Resilience
res Reason
name Transience
transience Reason
reason = Transience -> FailureAlignment -> Reason -> RuleEvaluation
Unavailable Transience
transience (Resilience -> FailureAlignment
resAlignment Resilience
res) (Reason
name Reason -> Reason -> Reason
forall a. Semigroup a => a -> a -> a
<> Reason
": " Reason -> Reason -> Reason
forall a. Semigroup a => a -> a -> a
<> Reason
reason)
transientCause :: EffectfulConfig -> Transience
transientCause :: EffectfulConfig -> Transience
transientCause EffectfulConfig
cfg = Maybe RetryAfter -> Transience
WillResolve (EffectfulConfig -> Maybe RetryAfter
ecRetryAfter EffectfulConfig
cfg)
admitProbe :: Resilience -> UTCTime -> IO Bool
admitProbe :: Resilience -> UTCTime -> IO Bool
admitProbe Resilience
res UTCTime
now = do
(permitted, old, new) <- STM (Bool, Breaker, Breaker) -> IO (Bool, Breaker, Breaker)
forall (m :: * -> *) a. MonadIO m => STM a -> m a
atomically (STM (Bool, Breaker, Breaker) -> IO (Bool, Breaker, Breaker))
-> STM (Bool, Breaker, Breaker) -> IO (Bool, Breaker, Breaker)
forall a b. (a -> b) -> a -> b
$ do
st <- TVar Breaker -> STM Breaker
forall a. TVar a -> STM a
readTVar (Resilience -> TVar Breaker
resBreaker Resilience
res)
let (p, st') = admit now st
writeTVar (resBreaker res) st'
pure (p, st, st')
reportBreakerChange (resBreakerReporter res) old new
pure permitted
commitBreaker :: Resilience -> (Breaker -> Breaker) -> IO ()
commitBreaker :: Resilience -> (Breaker -> Breaker) -> IO ()
commitBreaker Resilience
res Breaker -> Breaker
step = do
(old, new) <- STM (Breaker, Breaker) -> IO (Breaker, Breaker)
forall (m :: * -> *) a. MonadIO m => STM a -> m a
atomically (STM (Breaker, Breaker) -> IO (Breaker, Breaker))
-> STM (Breaker, Breaker) -> IO (Breaker, Breaker)
forall a b. (a -> b) -> a -> b
$ do
st <- TVar Breaker -> STM Breaker
forall a. TVar a -> STM a
readTVar (Resilience -> TVar Breaker
resBreaker Resilience
res)
let st' = Breaker -> Breaker
step Breaker
st
writeTVar (resBreaker res) st'
pure (st, st')
reportBreakerChange (resBreakerReporter res) old new
tripOnFailure :: EffectfulConfig -> UTCTime -> Breaker -> Breaker
tripOnFailure :: EffectfulConfig -> UTCTime -> Breaker -> Breaker
tripOnFailure EffectfulConfig
cfg = Int -> NominalDiffTime -> UTCTime -> Breaker -> Breaker
recordFailure (EffectfulConfig -> Int
ecBreakerThreshold EffectfulConfig
cfg) (EffectfulConfig -> NominalDiffTime
ecBreakerCooldown EffectfulConfig
cfg)
data EffectfulConfig = EffectfulConfig
{ EffectfulConfig -> Int
ecTimeout :: Int
, EffectfulConfig -> [Int]
ecBackoff :: [Int]
, EffectfulConfig -> Int
ecBreakerThreshold :: Int
, EffectfulConfig -> NominalDiffTime
ecBreakerCooldown :: NominalDiffTime
, EffectfulConfig -> Maybe RetryAfter
ecRetryAfter :: Maybe RetryAfter
}
defaultEffectfulConfig :: EffectfulConfig
defaultEffectfulConfig :: EffectfulConfig
defaultEffectfulConfig =
EffectfulConfig
{ ecTimeout :: Int
ecTimeout = Int
2_000_000
, ecBackoff :: [Int]
ecBackoff = [Int
100_000, Int
250_000]
, ecBreakerThreshold :: Int
ecBreakerThreshold = Int
5
, ecBreakerCooldown :: NominalDiffTime
ecBreakerCooldown = NominalDiffTime
30
, ecRetryAfter :: Maybe RetryAfter
ecRetryAfter = Maybe RetryAfter
forall a. Maybe a
Nothing
}
newBreaker :: IO (TVar Breaker)
newBreaker :: IO (TVar Breaker)
newBreaker = Breaker -> IO (TVar Breaker)
forall (m :: * -> *) a. MonadIO m => a -> m (TVar a)
newTVarIO Breaker
initialBreaker