{-# LANGUAGE RankNTypes            #-}

-----------------------------------------------------------------------------
-- |
-- Module      :  UnliftIO.Retry
-- Copyright   :  Ozgun Ataman <ozgun.ataman@soostone.com>
-- License     :  BSD3
--
-- Maintainer  :  Patrick Brisbin <pbrisbin@gmail.com>
-- Stability   :  provisional
--
-- Unlifted "Control.Retry".
--
-- @since 0.9.3.0
----------------------------------------------------------------------------


module UnliftIO.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.Retry hiding
    ( recoverAll
    , recovering
    , recoveringDynamic
    , resumeRecovering
    , resumeRecoveringDynamic
    , resumeRecoverAll
    , stepping
    )
import qualified Control.Retry as Retry
import           Control.Monad.Catch (Handler(..))
import           Control.Monad.IO.Unlift (MonadUnliftIO, withRunInIO)
import           Prelude
-------------------------------------------------------------------------------


-------------------------------------------------------------------------------
-- | 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
    :: MonadUnliftIO m
    => 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 :: forall (m :: * -> *) a.
MonadUnliftIO m =>
RetryPolicyM m
-> [RetryStatus -> Handler m Bool] -> (RetryStatus -> m a) -> m a
recovering = forall (m :: * -> *) a.
MonadUnliftIO m =>
RetryStatus
-> RetryPolicyM m
-> [RetryStatus -> Handler m Bool]
-> (RetryStatus -> m a)
-> m a
resumeRecovering RetryStatus
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
    :: MonadUnliftIO m
    => 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 :: forall (m :: * -> *) a.
MonadUnliftIO m =>
RetryStatus
-> RetryPolicyM m
-> [RetryStatus -> Handler m Bool]
-> (RetryStatus -> m a)
-> m a
resumeRecovering RetryStatus
retryStatus RetryPolicyM m
policy [RetryStatus -> Handler m Bool]
hs RetryStatus -> m a
f = forall (m :: * -> *) b.
MonadUnliftIO m =>
((forall a. m a -> IO a) -> IO b) -> m b
withRunInIO forall a b. (a -> b) -> a -> b
$ \forall a. m a -> IO a
runInIO ->
    forall (m :: * -> *) a.
(MonadIO m, MonadMask m) =>
RetryStatus
-> RetryPolicyM m
-> [RetryStatus -> Handler m Bool]
-> (RetryStatus -> m a)
-> m a
Retry.resumeRecovering
        RetryStatus
retryStatus
        (forall (m :: * -> *) (n :: * -> *).
(forall a. m a -> n a) -> RetryPolicyM m -> RetryPolicyM n
transRetryPolicy forall a. m a -> IO a
runInIO RetryPolicyM m
policy)
        (forall a b. (a -> b) -> [a] -> [b]
map (forall b c a. (b -> c) -> (a -> b) -> a -> c
(.) forall a b. (a -> b) -> a -> b
$ forall (m :: * -> *) (n :: * -> *) a.
(forall b. m b -> n b) -> Handler m a -> Handler n a
transHandler forall a. m a -> IO a
runInIO) [RetryStatus -> Handler m Bool]
hs)
        (forall a. m a -> IO a
runInIO forall b c a. (b -> c) -> (a -> b) -> a -> c
. RetryStatus -> m a
f)


-------------------------------------------------------------------------------
-- | The difference between this and 'recovering' is the same as
--  the difference between 'retryingDynamic' and 'retrying'.
recoveringDynamic
    :: MonadUnliftIO m
    => 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 :: forall (m :: * -> *) a.
MonadUnliftIO m =>
RetryPolicyM m
-> [RetryStatus -> Handler m RetryAction]
-> (RetryStatus -> m a)
-> m a
recoveringDynamic = forall (m :: * -> *) a.
MonadUnliftIO m =>
RetryStatus
-> RetryPolicyM m
-> [RetryStatus -> Handler m RetryAction]
-> (RetryStatus -> m a)
-> m a
resumeRecoveringDynamic RetryStatus
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
    :: MonadUnliftIO m
    => 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 :: forall (m :: * -> *) a.
MonadUnliftIO m =>
RetryStatus
-> RetryPolicyM m
-> [RetryStatus -> Handler m RetryAction]
-> (RetryStatus -> m a)
-> m a
resumeRecoveringDynamic RetryStatus
retryStatus RetryPolicyM m
policy [RetryStatus -> Handler m RetryAction]
hs RetryStatus -> m a
f = forall (m :: * -> *) b.
MonadUnliftIO m =>
((forall a. m a -> IO a) -> IO b) -> m b
withRunInIO forall a b. (a -> b) -> a -> b
$ \forall a. m a -> IO a
runInIO ->
    forall (m :: * -> *) a.
(MonadIO m, MonadMask m) =>
RetryStatus
-> RetryPolicyM m
-> [RetryStatus -> Handler m RetryAction]
-> (RetryStatus -> m a)
-> m a
Retry.resumeRecoveringDynamic
        RetryStatus
retryStatus
        (forall (m :: * -> *) (n :: * -> *).
(forall a. m a -> n a) -> RetryPolicyM m -> RetryPolicyM n
transRetryPolicy forall a. m a -> IO a
runInIO RetryPolicyM m
policy)
        (forall a b. (a -> b) -> [a] -> [b]
map (forall b c a. (b -> c) -> (a -> b) -> a -> c
(.) forall a b. (a -> b) -> a -> b
$ forall (m :: * -> *) (n :: * -> *) a.
(forall b. m b -> n b) -> Handler m a -> Handler n a
transHandler forall a. m a -> IO a
runInIO) [RetryStatus -> Handler m RetryAction]
hs)
        (forall a. m a -> IO a
runInIO forall b c a. (b -> c) -> (a -> b) -> a -> c
. RetryStatus -> m a
f)


-------------------------------------------------------------------------------
-- | 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
     :: MonadUnliftIO m
     => RetryPolicyM m
     -> (RetryStatus -> m a)
     -> m a
recoverAll :: forall (m :: * -> *) a.
MonadUnliftIO m =>
RetryPolicyM m -> (RetryStatus -> m a) -> m a
recoverAll = forall (m :: * -> *) a.
MonadUnliftIO m =>
RetryStatus -> RetryPolicyM m -> (RetryStatus -> m a) -> m a
resumeRecoverAll RetryStatus
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
     :: MonadUnliftIO m
     => RetryStatus
     -> RetryPolicyM m
     -> (RetryStatus -> m a)
     -> m a
resumeRecoverAll :: forall (m :: * -> *) a.
MonadUnliftIO m =>
RetryStatus -> RetryPolicyM m -> (RetryStatus -> m a) -> m a
resumeRecoverAll RetryStatus
retryStatus RetryPolicyM m
policy RetryStatus -> m a
f = forall (m :: * -> *) b.
MonadUnliftIO m =>
((forall a. m a -> IO a) -> IO b) -> m b
withRunInIO forall a b. (a -> b) -> a -> b
$ \forall a. m a -> IO a
runInIO ->
    forall (m :: * -> *) a.
(MonadIO m, MonadMask m) =>
RetryStatus -> RetryPolicyM m -> (RetryStatus -> m a) -> m a
Retry.resumeRecoverAll
        RetryStatus
retryStatus
        (forall (m :: * -> *) (n :: * -> *).
(forall a. m a -> n a) -> RetryPolicyM m -> RetryPolicyM n
transRetryPolicy forall a. m a -> IO a
runInIO RetryPolicyM m
policy)
        (forall a. m a -> IO a
runInIO forall b c a. (b -> c) -> (a -> b) -> a -> c
. RetryStatus -> m a
f)

-------------------------------------------------------------------------------
-- | 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
    :: MonadUnliftIO m
    => 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 :: forall (m :: * -> *) a.
MonadUnliftIO m =>
RetryPolicyM m
-> [RetryStatus -> Handler m Bool]
-> (RetryStatus -> m ())
-> (RetryStatus -> m a)
-> RetryStatus
-> m (Maybe a)
stepping RetryPolicyM m
policy [RetryStatus -> Handler m Bool]
hs RetryStatus -> m ()
schedule RetryStatus -> m a
f RetryStatus
s = forall (m :: * -> *) b.
MonadUnliftIO m =>
((forall a. m a -> IO a) -> IO b) -> m b
withRunInIO forall a b. (a -> b) -> a -> b
$ \forall a. m a -> IO a
runInIO ->
    forall (m :: * -> *) a.
(MonadIO m, MonadMask m) =>
RetryPolicyM m
-> [RetryStatus -> Handler m Bool]
-> (RetryStatus -> m ())
-> (RetryStatus -> m a)
-> RetryStatus
-> m (Maybe a)
Retry.stepping
        (forall (m :: * -> *) (n :: * -> *).
(forall a. m a -> n a) -> RetryPolicyM m -> RetryPolicyM n
transRetryPolicy forall a. m a -> IO a
runInIO RetryPolicyM m
policy)
        (forall a b. (a -> b) -> [a] -> [b]
map (forall b c a. (b -> c) -> (a -> b) -> a -> c
(.) forall a b. (a -> b) -> a -> b
$ forall (m :: * -> *) (n :: * -> *) a.
(forall b. m b -> n b) -> Handler m a -> Handler n a
transHandler forall a. m a -> IO a
runInIO) [RetryStatus -> Handler m Bool]
hs)
        (forall a. m a -> IO a
runInIO forall b c a. (b -> c) -> (a -> b) -> a -> c
. RetryStatus -> m ()
schedule)
        (forall a. m a -> IO a
runInIO forall b c a. (b -> c) -> (a -> b) -> a -> c
. RetryStatus -> m a
f)
        RetryStatus
s


-------------------------------------------------------------------------------
transRetryPolicy :: (forall a. m a -> n a) -> RetryPolicyM m -> RetryPolicyM n
transRetryPolicy :: forall (m :: * -> *) (n :: * -> *).
(forall a. m a -> n a) -> RetryPolicyM m -> RetryPolicyM n
transRetryPolicy forall a. m a -> n a
f (RetryPolicyM RetryStatus -> m (Maybe Int)
p) = forall (m :: * -> *).
(RetryStatus -> m (Maybe Int)) -> RetryPolicyM m
RetryPolicyM forall a b. (a -> b) -> a -> b
$ forall a. m a -> n a
f forall b c a. (b -> c) -> (a -> b) -> a -> c
. RetryStatus -> m (Maybe Int)
p


-------------------------------------------------------------------------------
transHandler :: (forall b. m b -> n b) -> Handler m a -> Handler n a
transHandler :: forall (m :: * -> *) (n :: * -> *) a.
(forall b. m b -> n b) -> Handler m a -> Handler n a
transHandler forall b. m b -> n b
f (Handler e -> m a
h) = forall (m :: * -> *) a e. Exception e => (e -> m a) -> Handler m a
Handler forall a b. (a -> b) -> a -> b
$ forall b. m b -> n b
f forall b c a. (b -> c) -> (a -> b) -> a -> c
. e -> m a
h