{-# LANGUAGE BangPatterns #-} {-# LANGUAGE CPP #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE MagicHash #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TupleSections #-} {-# LANGUAGE UnboxedTuples #-} {-# LANGUAGE ViewPatterns #-} ----------------------------------------------------------------------------- -- | -- Module : Control.Retry -- Copyright : Ozgun Ataman -- License : BSD3 -- -- Maintainer : Ozgun Ataman -- Stability : provisional -- -- This module exposes combinators that can wrap arbitrary monadic -- actions. They run the action and potentially retry running it with -- some configurable delay for a configurable number of times. -- -- The express purpose of this library is to make it easier to work -- with IO and especially network IO actions that often experience -- temporary failure that warrant retrying of the original action. For -- example, a database query may time out for a while, in which case -- we should delay a bit and retry the query. ---------------------------------------------------------------------------- module Control.Retry ( -- * Types and Operations RetryPolicyM (..) , RetryPolicy , retryPolicy , retryPolicyDefault , natTransformRetryPolicy , RetryAction (..) , toRetryAction , RetryStatus (..) , defaultRetryStatus , applyPolicy , applyAndDelay -- ** Lenses for 'RetryStatus' , rsIterNumberL , rsCumulativeDelayL , rsPreviousDelayL -- * Applying Retry Policies , retrying , retryingDynamic , recovering , recoveringDynamic , stepping , recoverAll , skipAsyncExceptions , logRetries , defaultLogMsg , retryOnError -- ** Resumable variants , resumeRetrying , resumeRetryingDynamic , resumeRecovering , resumeRecoveringDynamic , resumeRecoverAll -- * Retry Policies , constantDelay , exponentialBackoff , fullJitterBackoff , fibonacciBackoff , limitRetries -- * Policy Transformers , limitRetriesByDelay , limitRetriesByCumulativeDelay , capDelay -- * Development Helpers , simulatePolicy , simulatePolicyPP ) where ------------------------------------------------------------------------------- import Control.Applicative import Control.Concurrent #if MIN_VERSION_base(4, 7, 0) import Control.Exception (AsyncException, SomeAsyncException) #else import Control.Exception (AsyncException) #endif import Control.Monad import Control.Monad.Catch import Control.Monad.Except import Control.Monad.IO.Class as MIO import Control.Monad.Trans.Class as TC import Control.Monad.Trans.Maybe import Control.Monad.Trans.State import Data.List (foldl') import Data.Maybe import GHC.Generics import GHC.Prim import GHC.Types (Int(I#)) import System.Random # if MIN_VERSION_base(4, 9, 0) import Data.Semigroup # else import Data.Monoid # endif import Prelude ------------------------------------------------------------------------------- ------------------------------------------------------------------------------- -- | A 'RetryPolicyM' is a function that takes an 'RetryStatus' and -- possibly returns a delay in microseconds. Iteration numbers start -- at zero and increase by one on each retry. A *Nothing* return value from -- the function implies we have reached the retry limit. -- -- Please note that 'RetryPolicyM' is a 'Monoid'. You can collapse -- multiple strategies into one using 'mappend' or '<>'. The semantics -- of this combination are as follows: -- -- 1. If either policy returns 'Nothing', the combined policy returns -- 'Nothing'. This can be used to @inhibit@ after a number of retries, -- for example. -- -- 2. If both policies return a delay, the larger delay will be used. -- This is quite natural when combining multiple policies to achieve a -- certain effect. -- -- Example: -- -- One can easily define an exponential backoff policy with a limited -- number of retries: -- -- >> limitedBackoff = exponentialBackoff 50000 <> limitRetries 5 -- -- Naturally, 'mempty' will retry immediately (delay 0) for an -- unlimited number of retries, forming the identity for the 'Monoid'. -- -- The default retry policy 'retryPolicyDefault' implements a constant 50ms delay, up to 5 times: -- -- >> retryPolicyDefault = constantDelay 50000 <> limitRetries 5 -- -- For anything more complex, just define your own 'RetryPolicyM': -- -- >> myPolicy = retryPolicy $ \ rs -> if rsIterNumber rs > 10 then Just 1000 else Just 10000 -- -- Since 0.7. newtype RetryPolicyM m = RetryPolicyM { getRetryPolicyM :: RetryStatus -> m (Maybe Int) } -- | Simplified 'RetryPolicyM' without any use of the monadic context in -- determining policy. Mostly maintains backwards compatitibility with -- type signatures pre-0.7. type RetryPolicy = forall m . Monad m => RetryPolicyM m -- | Default retry policy retryPolicyDefault :: (Monad m) => RetryPolicyM m retryPolicyDefault = constantDelay 50000 <> limitRetries 5 -- Base 4.9.0 adds a Data.Semigroup module. This has fewer -- dependencies than the semigroups package, so we're using base's -- only if its available. # if MIN_VERSION_base(4, 9, 0) instance Monad m => Semigroup (RetryPolicyM m) where (RetryPolicyM a) <> (RetryPolicyM b) = RetryPolicyM $ \ n -> runMaybeT $ do a' <- MaybeT $ a n b' <- MaybeT $ b n return $! max a' b' instance Monad m => Monoid (RetryPolicyM m) where mempty = retryPolicy $ const (Just 0) mappend = (<>) # else instance Monad m => Monoid (RetryPolicyM m) where mempty = retryPolicy $ const (Just 0) (RetryPolicyM a) `mappend` (RetryPolicyM b) = RetryPolicyM $ \ n -> runMaybeT $ do a' <- MaybeT $ a n b' <- MaybeT $ b n return $! max a' b' #endif ------------------------------------------------------------------------------- -- | Applies a natural transformation to a policy to run a RetryPolicy -- meant for the monad @m@ in the monad @n@ provided a transformation -- from @m@ to @n@ is available. A common case is if you have a pure -- policy, @RetryPolicyM Identity@ and want to use it to govern an -- @IO@ computation you could write: -- -- @ -- purePolicyInIO :: RetryPolicyM Identity -> RetryPolicyM IO -- purePolicyInIO = natTransformRetryPolicy (pure . runIdentity) -- @ natTransformRetryPolicy :: (forall a. m a -> n a) -> RetryPolicyM m -> RetryPolicyM n natTransformRetryPolicy f (RetryPolicyM p) = RetryPolicyM $ \stat -> f (p stat) -- | Modify the delay of a RetryPolicy. -- Does not change whether or not a retry is performed. modifyRetryPolicyDelay :: Functor m => (Int -> Int) -> RetryPolicyM m -> RetryPolicyM m modifyRetryPolicyDelay f (RetryPolicyM p) = RetryPolicyM $ \stat -> fmap f <$> p stat ------------------------------------------------------------------------------- -- | How to handle a failed action. data RetryAction = DontRetry -- ^ Don't retry (regardless of what the 'RetryPolicy' says). | ConsultPolicy -- ^ Retry if the 'RetryPolicy' says so, with the delay specified by the policy. | ConsultPolicyOverrideDelay Int -- ^ Retry if the 'RetryPolicy' says so, but override the policy's delay (number of microseconds). deriving (Read, Show, Eq, Generic) -- | Convert a boolean answer to the question "Should we retry?" into -- a 'RetryAction'. toRetryAction :: Bool -> RetryAction toRetryAction False = DontRetry toRetryAction True = ConsultPolicy ------------------------------------------------------------------------------- -- | Datatype with stats about retries made thus far. data RetryStatus = RetryStatus { rsIterNumber :: !Int -- ^ Iteration number, where 0 is the first try , rsCumulativeDelay :: !Int -- ^ Delay incurred so far from retries in microseconds , rsPreviousDelay :: !(Maybe Int) -- ^ Latest attempt's delay. Will always be Nothing on first run. } deriving (Read, Show, Eq, Generic) ------------------------------------------------------------------------------- -- | Initial, default retry status. Use fields or lenses to update. defaultRetryStatus :: RetryStatus defaultRetryStatus = RetryStatus 0 0 Nothing ------------------------------------------------------------------------------- rsIterNumberL :: Lens' RetryStatus Int rsIterNumberL = lens rsIterNumber (\rs x -> rs { rsIterNumber = x }) {-# INLINE rsIterNumberL #-} ------------------------------------------------------------------------------- rsCumulativeDelayL :: Lens' RetryStatus Int rsCumulativeDelayL = lens rsCumulativeDelay (\rs x -> rs { rsCumulativeDelay = x }) {-# INLINE rsCumulativeDelayL #-} ------------------------------------------------------------------------------- rsPreviousDelayL :: Lens' RetryStatus (Maybe Int) rsPreviousDelayL = lens rsPreviousDelay (\rs x -> rs { rsPreviousDelay = x }) {-# INLINE rsPreviousDelayL #-} ------------------------------------------------------------------------------- -- | Apply policy on status to see what the decision would be. -- 'Nothing' implies no retry, 'Just' returns updated status. applyPolicy :: Monad m => RetryPolicyM m -> RetryStatus -> m (Maybe RetryStatus) applyPolicy (RetryPolicyM policy) s = do res <- policy s case res of Just delay -> return $! Just $! RetryStatus { rsIterNumber = rsIterNumber s + 1 , rsCumulativeDelay = rsCumulativeDelay s `boundedPlus` delay , rsPreviousDelay = Just delay } Nothing -> return Nothing ------------------------------------------------------------------------------- -- | Apply policy and delay by its amount if it results in a retry. -- Return updated status. applyAndDelay :: MIO.MonadIO m => RetryPolicyM m -> RetryStatus -> m (Maybe RetryStatus) applyAndDelay policy s = do chk <- applyPolicy policy s case chk of Just rs -> do case rsPreviousDelay rs of Nothing -> return () Just delay -> liftIO $ threadDelay delay return (Just rs) Nothing -> return Nothing ------------------------------------------------------------------------------- -- | Helper for making simplified policies that don't use the monadic -- context. retryPolicy :: (Monad m) => (RetryStatus -> Maybe Int) -> RetryPolicyM m retryPolicy f = RetryPolicyM $ \ s -> return (f s) ------------------------------------------------------------------------------- -- | Retry immediately, but only up to @n@ times. limitRetries :: Int -- ^ Maximum number of retries. -> RetryPolicy limitRetries i = retryPolicy $ \ RetryStatus { rsIterNumber = n} -> if n >= i then Nothing else Just 0 ------------------------------------------------------------------------------- -- | Add an upperbound to a policy such that once the given time-delay -- amount *per try* has been reached or exceeded, the policy will stop -- retrying and fail. If you need to stop retrying once *cumulative* -- delay reaches a time-delay amount, use -- 'limitRetriesByCumulativeDelay' limitRetriesByDelay :: Monad m => Int -- ^ Time-delay limit in microseconds. -> RetryPolicyM m -> RetryPolicyM m limitRetriesByDelay i p = RetryPolicyM $ \ n -> (>>= limit) `fmap` getRetryPolicyM p n where limit delay = if delay >= i then Nothing else Just delay ------------------------------------------------------------------------------- -- | Add an upperbound to a policy such that once the cumulative delay -- over all retries has reached or exceeded the given limit, the -- policy will stop retrying and fail. limitRetriesByCumulativeDelay :: Monad m => Int -- ^ Time-delay limit in microseconds. -> RetryPolicyM m -> RetryPolicyM m limitRetriesByCumulativeDelay cumulativeLimit p = RetryPolicyM $ \ stat -> (>>= limit stat) `fmap` getRetryPolicyM p stat where limit status curDelay | rsCumulativeDelay status `boundedPlus` curDelay > cumulativeLimit = Nothing | otherwise = Just curDelay ------------------------------------------------------------------------------- -- | Implement a constant delay with unlimited retries. constantDelay :: (Monad m) => Int -- ^ Base delay in microseconds -> RetryPolicyM m constantDelay delay = retryPolicy (const (Just delay)) ------------------------------------------------------------------------------- -- | Grow delay exponentially each iteration. Each delay will -- increase by a factor of two. exponentialBackoff :: (Monad m) => Int -- ^ Base delay in microseconds -> RetryPolicyM m exponentialBackoff base = retryPolicy $ \ RetryStatus { rsIterNumber = n } -> Just $! base `boundedMult` boundedPow 2 n ------------------------------------------------------------------------------- -- | FullJitter exponential backoff as explained in AWS Architecture -- Blog article. -- -- @http:\/\/www.awsarchitectureblog.com\/2015\/03\/backoff.html@ -- -- temp = min(cap, base * 2 ** attempt) -- -- sleep = temp \/ 2 + random_between(0, temp \/ 2) fullJitterBackoff :: (MonadIO m) => Int -- ^ Base delay in microseconds -> RetryPolicyM m fullJitterBackoff base = RetryPolicyM $ \ RetryStatus { rsIterNumber = n } -> do let d = (base `boundedMult` boundedPow 2 n) `div` 2 rand <- liftIO $ randomRIO (0, d) return $! Just $! d `boundedPlus` rand ------------------------------------------------------------------------------- -- | Implement Fibonacci backoff. fibonacciBackoff :: (Monad m) => Int -- ^ Base delay in microseconds -> RetryPolicyM m fibonacciBackoff base = retryPolicy $ \RetryStatus { rsIterNumber = n } -> Just $ fib (n + 1) (0, base) where fib 0 (a, _) = a fib !m (!a, !b) = fib (m-1) (b, a `boundedPlus` b) ------------------------------------------------------------------------------- -- | Set a time-upperbound for any delays that may be directed by the -- given policy. This function does not terminate the retrying. The policy -- `capDelay maxDelay (exponentialBackoff n)` will never stop retrying. It -- will reach a state where it retries forever with a delay of `maxDelay` -- between each one. To get termination you need to use one of the -- 'limitRetries' function variants. capDelay :: Monad m => Int -- ^ A maximum delay in microseconds -> RetryPolicyM m -> RetryPolicyM m capDelay limit p = RetryPolicyM $ \ n -> fmap (min limit) `fmap` getRetryPolicyM p n ------------------------------------------------------------------------------- -- | Retry combinator for actions that don't raise exceptions, but -- signal in their type the outcome has failed. Examples are the -- 'Maybe', 'Either' and 'EitherT' monads. -- -- Let's write a function that always fails and watch this combinator -- retry it 5 additional times following the initial run: -- -- >>> import Data.Maybe -- >>> let f _ = putStrLn "Running action" >> return Nothing -- >>> retrying retryPolicyDefault (const $ return . isNothing) f -- Running action -- Running action -- Running action -- Running action -- Running action -- Running action -- Nothing -- -- Note how the latest failing result is returned after all retries -- have been exhausted. retrying :: MonadIO m => RetryPolicyM m -> (RetryStatus -> b -> m Bool) -- ^ An action to check whether the result should be retried. -- If True, we delay and retry the operation. -> (RetryStatus -> m b) -- ^ Action to run -> m b retrying = resumeRetrying defaultRetryStatus ------------------------------------------------------------------------------- -- | A variant of 'retrying' that allows specifying the initial -- 'RetryStatus' so that the retrying operation may pick up where it left -- off in regards to its retry policy. resumeRetrying :: MonadIO m => RetryStatus -> RetryPolicyM m -> (RetryStatus -> b -> m Bool) -- ^ An action to check whether the result should be retried. -- If True, we delay and retry the operation. -> (RetryStatus -> m b) -- ^ Action to run -> m b resumeRetrying retryStatus policy chk f = resumeRetryingDynamic retryStatus policy (\rs -> fmap toRetryAction . chk rs) f ------------------------------------------------------------------------------- -- | Same as 'retrying', but with the ability to override -- the delay of the retry policy based on information -- obtained after initiation. -- -- For example, if the action to run is a HTTP request that -- turns out to fail with a status code 429 ("too many requests"), -- the response may contain a "Retry-After" HTTP header which -- specifies the number of seconds -- the client should wait until performing the next request. -- This function allows overriding the delay calculated by the given -- retry policy with the delay extracted from this header value. -- -- In other words, given an arbitrary 'RetryPolicyM' @rp@, the -- following invocation will always delay by 1000 microseconds: -- -- > retryingDynamic rp (\_ _ -> return $ ConsultPolicyOverrideDelay 1000) f -- -- Note that a 'RetryPolicy's decision to /not/ perform a retry -- cannot be overridden. Ie. /when/ to /stop/ retrying is always decided -- by the retry policy, regardless of the returned 'RetryAction' value. retryingDynamic :: MonadIO m => RetryPolicyM m -> (RetryStatus -> b -> m RetryAction) -- ^ An action to check whether the result should be retried. -- The returned 'RetryAction' determines how/if a retry is performed. -- See documentation on 'RetryAction'. -> (RetryStatus -> m b) -- ^ Action to run -> m b retryingDynamic = resumeRetryingDynamic defaultRetryStatus ------------------------------------------------------------------------------- -- | A variant of 'retryingDynamic' that allows specifying the initial -- 'RetryStatus' so that a retrying operation may pick up where it left off -- in regards to its retry policy. resumeRetryingDynamic :: MonadIO m => RetryStatus -> RetryPolicyM m -> (RetryStatus -> b -> m RetryAction) -- ^ An action to check whether the result should be retried. -- The returned 'RetryAction' determines how/if a retry is performed. -- See documentation on 'RetryAction'. -> (RetryStatus -> m b) -- ^ Action to run -> m b resumeRetryingDynamic retryStatus policy chk f = go retryStatus where go s = do res <- f s let consultPolicy policy' = do rs <- applyAndDelay policy' s case rs of Nothing -> return res Just rs' -> go $! rs' chk' <- chk s res case chk' of DontRetry -> return res ConsultPolicy -> consultPolicy policy ConsultPolicyOverrideDelay delay -> consultPolicy $ modifyRetryPolicyDelay (const delay) policy ------------------------------------------------------------------------------- -- | Retry ALL exceptions that may be raised. To be used with caution; -- this matches the exception on 'SomeException'. Note that this -- handler explicitly does not handle 'AsyncException' nor -- 'SomeAsyncException' (for versions of base >= 4.7). It is not a -- good idea to catch async exceptions as it can result in hanging -- threads and programs. Note that if you just throw an exception to -- this thread that does not descend from SomeException, recoverAll -- will not catch it. -- -- See how the action below is run once and retried 5 more times -- before finally failing for good: -- -- >>> let f _ = putStrLn "Running action" >> error "this is an error" -- >>> recoverAll retryPolicyDefault f -- Running action -- Running action -- Running action -- Running action -- Running action -- Running action -- *** Exception: this is an error recoverAll #if MIN_VERSION_exceptions(0, 6, 0) :: (MonadIO m, MonadMask m) #else :: (MonadIO m, MonadCatch m) #endif => RetryPolicyM m -> (RetryStatus -> m a) -> m a recoverAll = resumeRecoverAll defaultRetryStatus ------------------------------------------------------------------------------- -- | A variant of 'recoverAll' that allows specifying the initial -- 'RetryStatus' so that a recovering operation may pick up where it left -- off in regards to its retry policy. resumeRecoverAll #if MIN_VERSION_exceptions(0, 6, 0) :: (MonadIO m, MonadMask m) #else :: (MonadIO m, MonadCatch m) #endif => RetryStatus -> RetryPolicyM m -> (RetryStatus -> m a) -> m a resumeRecoverAll retryStatus set f = resumeRecovering retryStatus set handlers f where handlers = skipAsyncExceptions ++ [h] h _ = Handler $ \ (_ :: SomeException) -> return True ------------------------------------------------------------------------------- -- | List of pre-made handlers that will skip retries on -- 'AsyncException' and 'SomeAsyncException'. Append your handlers to -- this list as a convenient way to make sure you're not catching -- async exceptions like user interrupt. skipAsyncExceptions :: ( MonadIO m ) => [RetryStatus -> Handler m Bool] skipAsyncExceptions = handlers where asyncH _ = Handler $ \ (_ :: AsyncException) -> return False #if MIN_VERSION_base(4, 7, 0) someAsyncH _ = Handler $ \(_ :: SomeAsyncException) -> return False handlers = [asyncH, someAsyncH] #else handlers = [asyncH] #endif ------------------------------------------------------------------------------- -- | Run an action and recover from a raised exception by potentially -- retrying the action a number of times. Note that if you're going to -- use a handler for 'SomeException', you should add explicit cases -- *earlier* in the list of handlers to reject 'AsyncException' and -- 'SomeAsyncException', as catching these can cause thread and -- program hangs. 'recoverAll' already does this for you so if you -- just plan on catching 'SomeException', you may as well use -- 'recoverAll' recovering #if MIN_VERSION_exceptions(0, 6, 0) :: (MonadIO m, MonadMask m) #else :: (MonadIO m, MonadCatch m) #endif => RetryPolicyM m -- ^ Just use 'retryPolicyDefault' for default settings -> [RetryStatus -> Handler m Bool] -- ^ Should a given exception be retried? Action will be -- retried if this returns True *and* the policy allows it. -- This action will be consulted first even if the policy -- later blocks it. -> (RetryStatus -> m a) -- ^ Action to perform -> m a recovering = resumeRecovering defaultRetryStatus ------------------------------------------------------------------------------- -- | A variant of 'recovering' that allows specifying the initial -- 'RetryStatus' so that a recovering operation may pick up where it left -- off in regards to its retry policy. resumeRecovering #if MIN_VERSION_exceptions(0, 6, 0) :: (MonadIO m, MonadMask m) #else :: (MonadIO m, MonadCatch m) #endif => RetryStatus -> RetryPolicyM m -- ^ Just use 'retryPolicyDefault' for default settings -> [(RetryStatus -> Handler m Bool)] -- ^ Should a given exception be retried? Action will be -- retried if this returns True *and* the policy allows it. -- This action will be consulted first even if the policy -- later blocks it. -> (RetryStatus -> m a) -- ^ Action to perform -> m a resumeRecovering retryStatus policy hs f = resumeRecoveringDynamic retryStatus policy hs' f where hs' = map (fmap toRetryAction .) hs ------------------------------------------------------------------------------- -- | The difference between this and 'recovering' is the same as -- the difference between 'retryingDynamic' and 'retrying'. recoveringDynamic #if MIN_VERSION_exceptions(0, 6, 0) :: (MonadIO m, MonadMask m) #else :: (MonadIO m, MonadCatch m) #endif => RetryPolicyM m -- ^ Just use 'retryPolicyDefault' for default settings -> [RetryStatus -> Handler m RetryAction] -- ^ Should a given exception be retried? Action will be -- retried if this returns either 'ConsultPolicy' or -- 'ConsultPolicyOverrideDelay' *and* the policy allows it. -- This action will be consulted first even if the policy -- later blocks it. -> (RetryStatus -> m a) -- ^ Action to perform -> m a recoveringDynamic = resumeRecoveringDynamic defaultRetryStatus ------------------------------------------------------------------------------- -- | A variant of 'recoveringDynamic' that allows specifying the initial -- 'RetryStatus' so that a recovering operation may pick up where it left -- off in regards to its retry policy. resumeRecoveringDynamic #if MIN_VERSION_exceptions(0, 6, 0) :: (MonadIO m, MonadMask m) #else :: (MonadIO m, MonadCatch m) #endif => RetryStatus -> RetryPolicyM m -- ^ Just use 'retryPolicyDefault' for default settings -> [(RetryStatus -> Handler m RetryAction)] -- ^ Should a given exception be retried? Action will be -- retried if this returns either 'ConsultPolicy' or -- 'ConsultPolicyOverrideDelay' *and* the policy allows it. -- This action will be consulted first even if the policy -- later blocks it. -> (RetryStatus -> m a) -- ^ Action to perform -> m a resumeRecoveringDynamic retryStatus policy hs f = mask $ \restore -> go restore retryStatus where go restore = loop where loop s = do r <- try $ restore (f s) case r of Right x -> return x Left e -> recover (e :: SomeException) hs where recover e [] = throwM e recover e ((($ s) -> Handler h) : hs') | Just e' <- fromException e = do let consultPolicy policy' = do rs <- applyAndDelay policy' s case rs of Just rs' -> loop $! rs' Nothing -> throwM e' chk <- h e' case chk of DontRetry -> throwM e' ConsultPolicy -> consultPolicy policy ConsultPolicyOverrideDelay delay -> consultPolicy $ modifyRetryPolicyDelay (const delay) policy | otherwise = recover e hs' ------------------------------------------------------------------------------- -- | A version of 'recovering' that tries to run the action only a -- single time. The control will return immediately upon both success -- and failure. Useful for implementing retry logic in distributed -- queues and similar external-interfacing systems. stepping #if MIN_VERSION_exceptions(0, 6, 0) :: (MonadIO m, MonadMask m) #else :: (MonadIO m, MonadCatch m) #endif => RetryPolicyM m -- ^ Just use 'retryPolicyDefault' for default settings -> [RetryStatus -> Handler m Bool] -- ^ Should a given exception be retried? Action will be -- retried if this returns True *and* the policy allows it. -- This action will be consulted first even if the policy -- later blocks it. -> (RetryStatus -> m ()) -- ^ Action to run with updated status upon failure. -> (RetryStatus -> m a) -- ^ Main action to perform with current status. -> RetryStatus -- ^ Current status of this step -> m (Maybe a) stepping policy hs schedule f s = do r <- try $ f s case r of Right x -> return $ Just x Left e -> recover (e :: SomeException) hs where recover e [] = throwM e recover e ((($ s) -> Handler h) : hs') | Just e' <- fromException e = do chk <- h e' case chk of True -> do res <- applyPolicy policy s case res of Just rs -> do schedule $! rs return Nothing Nothing -> throwM e' False -> throwM e' | otherwise = recover e hs' ------------------------------------------------------------------------------- -- | Helper function for constructing handler functions of the form required -- by 'recovering'. logRetries :: ( Monad m , Exception e) => (e -> m Bool) -- ^ Test for whether action is to be retried -> (Bool -> e -> RetryStatus -> m ()) -- ^ How to report the generated warning message. Boolean is -- whether it's being retried or crashed. -> RetryStatus -- ^ Retry number -> Handler m Bool logRetries test reporter status = Handler $ \ err -> do result <- test err reporter result err status return result -- | For use with 'logRetries'. defaultLogMsg :: (Exception e) => Bool -> e -> RetryStatus -> String defaultLogMsg shouldRetry err status = "[retry:" <> iter <> "] Encountered " <> show err <> ". " <> nextMsg where iter = show $ rsIterNumber status nextMsg = if shouldRetry then "Retrying." else "Crashing." ------------------------------------------------------------------------------- retryOnError :: (Functor m, MonadIO m, MonadError e m) => RetryPolicyM m -- ^ Policy -> (RetryStatus -> e -> m Bool) -- ^ Should an error be retried? -> (RetryStatus -> m a) -- ^ Action to perform -> m a retryOnError policy chk f = go defaultRetryStatus where go stat = do res <- (Right <$> f stat) `catchError` (\e -> Left . (e, ) <$> chk stat e) case res of Right x -> return x Left (e, True) -> do mstat' <- applyAndDelay policy stat case mstat' of Just stat' -> do go $! stat' Nothing -> throwError e Left (e, False) -> throwError e ------------------------------------------------------------------------------- -- | Run given policy up to N iterations and gather results. In the -- pair, the @Int@ is the iteration number and the @Maybe Int@ is the -- delay in microseconds. simulatePolicy :: Monad m => Int -> RetryPolicyM m -> m [(Int, Maybe Int)] simulatePolicy n (RetryPolicyM f) = flip evalStateT defaultRetryStatus $ forM [0..n] $ \i -> do stat <- get delay <- TC.lift (f stat) put $! stat { rsIterNumber = i + 1 , rsCumulativeDelay = rsCumulativeDelay stat `boundedPlus` fromMaybe 0 delay , rsPreviousDelay = delay } return (i, delay) ------------------------------------------------------------------------------- -- | Run given policy up to N iterations and pretty print results on -- the console. simulatePolicyPP :: Int -> RetryPolicyM IO -> IO () simulatePolicyPP n p = do ps <- simulatePolicy n p forM_ ps $ \ (iterNo, res) -> putStrLn $ show iterNo <> ": " <> maybe "Inhibit" ppTime res putStrLn $ "Total cumulative delay would be: " <> ppTime (boundedSum $ mapMaybe snd ps) ------------------------------------------------------------------------------- ppTime :: (Integral a, Show a) => a -> String ppTime n | n < 1000 = show n <> "us" | n < 1000000 = show ((fromIntegral n / 1000) :: Double) <> "ms" | otherwise = show ((fromIntegral n / 1000) :: Double) <> "ms" ------------------------------------------------------------------------------- -- Bounded arithmetic ------------------------------------------------------------------------------- -- | Same as '+' on 'Int' but it maxes out at @'maxBound' :: 'Int'@ or -- @'minBound' :: 'Int'@ rather than rolling over boundedPlus :: Int -> Int -> Int boundedPlus i@(I# i#) j@(I# j#) = case addIntC# i# j# of (# k#, 0# #) -> I# k# (# _, _ #) | maxBy abs i j < 0 -> minBound | otherwise -> maxBound where maxBy f a b = if f a >= f b then a else b -- | Same as '*' on 'Int' but it maxes out at @'maxBound' :: 'Int'@ or -- @'minBound' :: 'Int'@ rather than rolling over boundedMult :: Int -> Int -> Int boundedMult i@(I# i#) j@(I# j#) = case mulIntMayOflo# i# j# of 0# -> I# (i# *# j#) _ | signum i * signum j < 0 -> minBound | otherwise -> maxBound -- | Same as 'sum' on 'Int' but it maxes out at @'maxBound' :: 'Int'@ or -- @'minBound' :: 'Int'@ rather than rolling over boundedSum :: [Int] -> Int boundedSum = foldl' boundedPlus 0 -- | Same as '^' on 'Int' but it maxes out at @'maxBound' :: 'Int'@ or -- @'MinBound' :: 'Int'@ rather than rolling over boundedPow :: Int -> Int -> Int boundedPow x0 y0 | y0 < 0 = error "Negative exponent" | y0 == 0 = 1 | otherwise = f x0 y0 where f x y | even y = f (x `boundedMult` x) (y `quot` 2) | y == 1 = x | otherwise = g (x `boundedMult` x) ((y - 1) `quot` 2) x g x y z | even y = g (x `boundedMult` x) (y `quot` 2) z | y == 1 = x `boundedMult` z | otherwise = g (x `boundedMult` x) ((y - 1) `quot` 2) (x `boundedMult` z) ------------------------------------------------------------------------------- -- Lens machinery ------------------------------------------------------------------------------- -- Unexported type aliases to clean up the documentation type Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t type Lens' s a = Lens s s a a ------------------------------------------------------------------------------- lens :: (s -> a) -> (s -> b -> t) -> Lens s t a b lens sa sbt afb s = sbt s <$> afb (sa s) {-# INLINE lens #-} ------------------ -- Simple Tests -- ------------------ -- data TestException = TestException deriving (Show, Typeable) -- data AnotherException = AnotherException deriving (Show, Typeable) -- instance Exception TestException -- instance Exception AnotherException -- test = retrying retryPolicyDefault [h1,h2] f -- where -- f = putStrLn "Running action" >> throwM AnotherException -- h1 = Handler $ \ (e :: TestException) -> return False -- h2 = Handler $ \ (e :: AnotherException) -> return True