module Ecluse.Core.Rules.Effectful (
Resilience (..),
EffectfulConfig (..),
defaultEffectfulConfig,
newBreaker,
runResilient,
ReadFault (..),
) where
import Control.Retry (retrying)
import Data.Time (NominalDiffTime, UTCTime)
import UnliftIO (timeout, tryAny)
import Ecluse.Core.Breaker (
Breaker,
BreakerReporter,
admit,
initialBreaker,
recordFailure,
recordSuccess,
reportBreakerChange,
)
import Ecluse.Core.Rules.Types
import Ecluse.Core.Supervision (delayListPolicy)
import Ecluse.Core.Text (displayExceptionT)
data Resilience = Resilience
{ Resilience -> EffectfulConfig
resConfig :: EffectfulConfig
, Resilience -> TVar Breaker
resBreaker :: TVar Breaker
, Resilience -> BreakerReporter
resBreakerReporter :: BreakerReporter
, Resilience -> IO UTCTime
resClock :: IO UTCTime
}
data ReadFault = ReadFault
{ ReadFault -> Transience
rfTransience :: Transience
, ReadFault -> Text
rfReason :: Text
, ReadFault -> Text
rfDetail :: Text
}
deriving stock (ReadFault -> ReadFault -> Bool
(ReadFault -> ReadFault -> Bool)
-> (ReadFault -> ReadFault -> Bool) -> Eq ReadFault
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: ReadFault -> ReadFault -> Bool
== :: ReadFault -> ReadFault -> Bool
$c/= :: ReadFault -> ReadFault -> Bool
/= :: ReadFault -> ReadFault -> Bool
Eq, Int -> ReadFault -> ShowS
[ReadFault] -> ShowS
ReadFault -> String
(Int -> ReadFault -> ShowS)
-> (ReadFault -> String)
-> ([ReadFault] -> ShowS)
-> Show ReadFault
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> ReadFault -> ShowS
showsPrec :: Int -> ReadFault -> ShowS
$cshow :: ReadFault -> String
show :: ReadFault -> String
$cshowList :: [ReadFault] -> ShowS
showList :: [ReadFault] -> ShowS
Show)
runResilient :: Resilience -> IO a -> IO (Either ReadFault a)
runResilient :: forall a. Resilience -> IO a -> IO (Either ReadFault a)
runResilient Resilience
res IO a
act = 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 (Left (ReadFault (transientCause (resConfig res)) breakerOpen breakerOpen))
else do
result <- attemptWithRetry res act
settledNow <- resClock res
settleOutcome res settledNow result
where
breakerOpen :: Text
breakerOpen = Text
"the rule source circuit breaker is open"
settleOutcome :: Resilience -> UTCTime -> Either (Transience, Text) a -> IO (Either ReadFault a)
settleOutcome :: forall a.
Resilience
-> UTCTime
-> Either (Transience, Text) a
-> IO (Either ReadFault a)
settleOutcome Resilience
res UTCTime
now = \case
Right a
value -> do
Resilience -> (Breaker -> Breaker) -> IO ()
commitBreaker Resilience
res Breaker -> Breaker
recordSuccess
Either ReadFault a -> IO (Either ReadFault a)
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (a -> Either ReadFault a
forall a b. b -> Either a b
Right a
value)
Left (Transience
transience, Text
detail) -> do
Resilience -> (Breaker -> Breaker) -> IO ()
commitBreaker Resilience
res (EffectfulConfig -> UTCTime -> Breaker -> Breaker
tripOnFailure (Resilience -> EffectfulConfig
resConfig Resilience
res) UTCTime
now)
Either ReadFault a -> IO (Either ReadFault a)
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (ReadFault -> Either ReadFault a
forall a b. a -> Either a b
Left (Transience -> Text -> Text -> ReadFault
ReadFault Transience
transience Text
"the rule could not be evaluated" Text
detail))
attemptWithRetry :: Resilience -> IO a -> IO (Either (Transience, Text) a)
attemptWithRetry :: forall a. Resilience -> IO a -> IO (Either (Transience, Text) a)
attemptWithRetry Resilience
res IO a
act =
RetryPolicyM IO
-> (RetryStatus -> Either (Transience, Text) a -> IO Bool)
-> (RetryStatus -> IO (Either (Transience, Text) a))
-> IO (Either (Transience, Text) a)
forall (m :: * -> *) b.
MonadIO m =>
RetryPolicyM m
-> (RetryStatus -> b -> m Bool) -> (RetryStatus -> m b) -> m b
retrying ([Int] -> RetryPolicyM IO
forall (m :: * -> *). Monad m => [Int] -> RetryPolicyM m
delayListPolicy (EffectfulConfig -> [Int]
ecBackoff (Resilience -> EffectfulConfig
resConfig Resilience
res))) RetryStatus -> Either (Transience, Text) a -> IO Bool
forall {f :: * -> *} {p} {a} {b}.
Applicative f =>
p -> Either a b -> f Bool
shouldRetry (\RetryStatus
_ -> Resilience -> IO a -> IO (Either (Transience, Text) a)
forall a. Resilience -> IO a -> IO (Either (Transience, Text) a)
attemptOnce Resilience
res IO a
act)
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
attemptOnce :: Resilience -> IO a -> IO (Either (Transience, Text) a)
attemptOnce :: forall a. Resilience -> IO a -> IO (Either (Transience, Text) a)
attemptOnce Resilience
res IO a
act = do
result <- IO (Maybe a) -> IO (Either SomeException (Maybe a))
forall (m :: * -> *) a.
MonadUnliftIO m =>
m a -> m (Either SomeException a)
tryAny (Int -> IO a -> IO (Maybe a)
forall (m :: * -> *) a.
MonadUnliftIO m =>
Int -> m a -> m (Maybe a)
timeout (EffectfulConfig -> Int
ecTimeout (Resilience -> EffectfulConfig
resConfig Resilience
res)) IO a
act)
pure $ case result of
Left SomeException
e -> (Transience, Text) -> Either (Transience, Text) a
forall a b. a -> Either a b
Left (Transience
transient, Text
"the rule threw: " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> SomeException -> Text
forall e. Exception e => e -> Text
displayExceptionT SomeException
e)
Right Maybe a
Nothing -> (Transience, Text) -> Either (Transience, Text) a
forall a b. a -> Either a b
Left (Transience
transient, Text
"the attempt timed out")
Right (Just a
value) -> a -> Either (Transience, Text) a
forall a b. b -> Either a b
Right a
value
where
transient :: Transience
transient = EffectfulConfig -> Transience
transientCause (Resilience -> EffectfulConfig
resConfig Resilience
res)
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