packages feed

retry 0.7.5.1 → 0.7.6.0

raw patch · 6 files changed

+460/−25 lines, 6 filesdep +hedgehogdep +tastydep +tasty-hedgehogdep −QuickCheckdep −hspecdep ~HUnitPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: hedgehog, tasty, tasty-hedgehog, tasty-hunit

Dependencies removed: QuickCheck, hspec

Dependency ranges changed: HUnit

API changes (from Hackage documentation)

+ Control.Retry: limitRetriesByCumulativeDelay :: Monad m => Int -> RetryPolicyM m -> RetryPolicyM m
- Control.Retry: defaultLogMsg :: (Show e, Exception e) => Bool -> e -> RetryStatus -> String
+ Control.Retry: defaultLogMsg :: (Exception e) => Bool -> e -> RetryStatus -> String
- Control.Retry: limitRetriesByDelay :: Int -> RetryPolicy -> RetryPolicy
+ Control.Retry: limitRetriesByDelay :: Monad m => Int -> RetryPolicyM m -> RetryPolicyM m
- Control.Retry: logRetries :: (Monad m, Show e, Exception e) => (e -> m Bool) -> (Bool -> e -> RetryStatus -> m ()) -> RetryStatus -> Handler m Bool
+ Control.Retry: logRetries :: (Monad m, Exception e) => (e -> m Bool) -> (Bool -> e -> RetryStatus -> m ()) -> RetryStatus -> Handler m Bool
- Control.Retry: skipAsyncExceptions :: (MonadIO m, MonadMask m) => [RetryStatus -> Handler m Bool]
+ Control.Retry: skipAsyncExceptions :: (MonadIO m) => [RetryStatus -> Handler m Bool]

Files

changelog.md view
@@ -1,3 +1,7 @@+0.7.6.0+* Clarify the semantics of `limitRetriesByDelay`.+* Add `limitRetriesByCumulativeDelay`+ 0.7.5.1 * Improve haddocks for fullJitterBackoff. 
retry.cabal view
@@ -14,7 +14,7 @@         case we should hang back for a bit and retry the query instead         of simply raising an exception. -version:             0.7.5.1+version:             0.7.6.0 synopsis:            Retry combinators for monadic actions that may fail license:             BSD3 license-file:        LICENSE@@ -29,6 +29,10 @@   README.md   changelog.md +flag lib-Werror+  default: False+  manual: True+ library   exposed-modules:     Control.Retry   build-depends:@@ -41,12 +45,19 @@   hs-source-dirs:      src   default-language:    Haskell2010 +  if flag(lib-Werror)+    ghc-options: -Werror +  ghc-options: -Wall++ test-suite test     type:           exitcode-stdio-1.0-    main-is:        main.hs+    main-is:        Main.hs     hs-source-dirs: test,src     ghc-options:    -threaded+    other-modules:  Control.Retry+                    Tests.Control.Retry     build-depends:         base              ==4.*       , exceptions@@ -54,13 +65,20 @@       , data-default-class       , random       , time-      , QuickCheck         >= 2.7 && < 2.11-      , HUnit              >= 1.2.5.2 && < 1.7-      , hspec              >= 1.9+      , HUnit              >= 1.2.5.2+      , tasty+      , tasty-hunit+      , tasty-hedgehog+      , hedgehog       , stm       , ghc-prim       , mtl     default-language: Haskell2010++    if flag(lib-Werror)+      ghc-options: -Werror++    ghc-options: -Wall  source-repository head   type:     git
src/Control/Retry.hs view
@@ -64,6 +64,7 @@      -- * Policy Transformers     , limitRetriesByDelay+    , limitRetriesByCumulativeDelay     , capDelay      -- * Development Helpers@@ -73,7 +74,6 @@  ------------------------------------------------------------------------------- import           Control.Applicative-import           Control.Arrow import           Control.Concurrent #if MIN_VERSION_base(4, 7, 0) import           Control.Exception (AsyncException, SomeAsyncException)@@ -88,7 +88,6 @@ import           Control.Monad.Trans.State import           Data.Default.Class import           Data.List (foldl')-import           Data.Functor.Identity import           Data.Maybe import           GHC.Generics import           GHC.Prim@@ -99,7 +98,7 @@ # else import           Data.Monoid # endif-import           Prelude                hiding (catch)+import           Prelude -------------------------------------------------------------------------------  @@ -275,13 +274,16 @@  ------------------------------------------------------------------------------- -- | Add an upperbound to a policy such that once the given time-delay--- amount has been reached or exceeded, the policy will stop retrying--- and fail.+-- 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-    :: Int+    :: Monad m+    => Int     -- ^ Time-delay limit in microseconds.-    -> RetryPolicy-    -> RetryPolicy+    -> RetryPolicyM m+    -> RetryPolicyM m limitRetriesByDelay i p = RetryPolicyM $ \ n ->     (>>= limit) `liftM` getRetryPolicyM p n   where@@ -289,6 +291,24 @@   -------------------------------------------------------------------------------+-- | 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) `liftM` getRetryPolicyM p stat+  where+    limit status curDelay+      | rsCumulativeDelay status `boundedPlus` curDelay > cumulativeLimit = Nothing+      | otherwise = Just curDelay+++------------------------------------------------------------------------------- -- | Implement a constant delay with unlimited retries. constantDelay     :: Int@@ -442,8 +462,9 @@ -- '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, MonadMask m) +skipAsyncExceptions+    :: ( MonadIO m+       )     => [RetryStatus -> Handler m Bool] skipAsyncExceptions = handlers   where@@ -557,7 +578,8 @@ -- | Helper function for constructing handler functions of the form required -- by 'recovering'. logRetries-    :: (Monad m, Show e, Exception e)+    :: ( Monad m+       , Exception e)     => (e -> m Bool)     -- ^ Test for whether action is to be retried     -> (Bool -> e -> RetryStatus -> m ())@@ -572,12 +594,12 @@     return result  -- | For use with 'logRetries'.-defaultLogMsg :: (Show e, Exception e) => Bool -> e -> RetryStatus -> String+defaultLogMsg :: (Exception e) => Bool -> e -> RetryStatus -> String defaultLogMsg shouldRetry err status =-    "[retry:" <> iter <> "] Encountered " <> show err <> ". " <> next+    "[retry:" <> iter <> "] Encountered " <> show err <> ". " <> nextMsg   where     iter = show $ rsIterNumber status-    next = if shouldRetry then "Retrying." else "Crashing."+    nextMsg = if shouldRetry then "Retrying." else "Crashing."   -------------------------------------------------------------------------------@@ -602,8 +624,8 @@ simulatePolicyPP :: Int -> RetryPolicyM IO -> IO () simulatePolicyPP n p = do     ps <- simulatePolicy n p-    forM_ ps $ \ (n, res) -> putStrLn $-      show n <> ": " <> maybe "Inhibit" ppTime res+    forM_ ps $ \ (iterNo, res) -> putStrLn $+      show iterNo <> ": " <> maybe "Inhibit" ppTime res     putStrLn $ "Total cumulative delay would be: " <>       (ppTime $ boundedSum $ (mapMaybe snd) ps) @@ -611,8 +633,8 @@ ------------------------------------------------------------------------------- ppTime :: (Integral a, Show a) => a -> String ppTime n | n < 1000 = show n <> "us"-         | n < 1000000 = show (fromIntegral n / 1000) <> "ms"-         | otherwise = show (fromIntegral n / 1000) <> "ms"+         | n < 1000000 = show ((fromIntegral n / 1000) :: Double) <> "ms"+         | otherwise = show ((fromIntegral n / 1000) :: Double) <> "ms"  ------------------------------------------------------------------------------- -- Bounded arithmetic
+ test/Main.hs view
@@ -0,0 +1,22 @@+module Main+    ( main+    ) where+++-------------------------------------------------------------------------------+import           Test.Tasty+-------------------------------------------------------------------------------+import qualified Tests.Control.Retry+-------------------------------------------------------------------------------++++main :: IO ()+main = defaultMain tests+++-------------------------------------------------------------------------------+tests :: TestTree+tests = testGroup "retry"+  [ Tests.Control.Retry.tests+  ]
+ test/Tests/Control/Retry.hs view
@@ -0,0 +1,371 @@+{-# LANGUAGE DeriveDataTypeable  #-}+{-# LANGUAGE ScopedTypeVariables #-}+module Tests.Control.Retry+    ( tests+    ) where++-------------------------------------------------------------------------------+import           Control.Applicative+import           Control.Concurrent+import           Control.Concurrent.STM      as STM+import           Control.Exception           (AsyncException (..), IOException,+                                              MaskingState (..),+                                              getMaskingState, throwTo)+import           Control.Monad.Catch+import           Control.Monad.Identity+import           Control.Monad.IO.Class+import           Control.Monad.Writer.Strict+import           Data.Default.Class          (def)+import           Data.Either+import           Data.IORef+import           Data.List+import           Data.Maybe+import           Data.Time.Clock+import           Data.Time.LocalTime         ()+import           Data.Typeable+import           Hedgehog                    as HH+import qualified Hedgehog.Gen                as Gen+import qualified Hedgehog.Range              as Range+import           System.IO.Error+import           Test.Tasty+import           Test.Tasty.Hedgehog+import           Test.Tasty.HUnit            (assertBool, testCase, (@?=))+-------------------------------------------------------------------------------+import           Control.Retry+-------------------------------------------------------------------------------+++tests :: TestTree+tests = testGroup "Control.Retry"+  [ recoveringTests+  , monoidTests+  , retryStatusTests+  , quadraticDelayTests+  , policyTransformersTests+  , maskingStateTests+  , capDelayTests+  , limitRetriesByCumulativeDelayTests+  ]+++-------------------------------------------------------------------------------+recoveringTests :: TestTree+recoveringTests = testGroup "recovering"+  [ testProperty "recovering test without quadratic retry delay" $ property $ do+      startTime <- liftIO getCurrentTime+      timeout <- forAll (Gen.int (Range.linear 0 15))+      retries <- forAll (Gen.int (Range.linear 0 50))+      res <- liftIO $ try $ recovering+        (constantDelay timeout <> limitRetries retries)+        testHandlers+        (const $ throwM (userError "booo"))+      endTime <- liftIO getCurrentTime+      HH.assert (isLeftAnd isUserError res)+      let ms' = (fromInteger . toInteger $ (timeout * retries)) / 1000000.0+      HH.assert (diffUTCTime endTime startTime >= ms')+  , testGroup "exception hierarchy semantics"+      [ testCase "does not catch async exceptions" $ do+          counter <- newTVarIO (0 :: Int)+          done <- newEmptyMVar+          let work = atomically (modifyTVar' counter succ) >> threadDelay 1000000++          tid <- forkIO $+            recoverAll (limitRetries 2) (const work) `finally` putMVar done ()++          atomically (STM.check . (== 1) =<< readTVar counter)+          throwTo tid UserInterrupt++          takeMVar done++          count <- atomically (readTVar counter)+          count @?= 1++      , testCase "recovers from custom exceptions" $ do+          f <- mkFailN Custom1 2+          res <- try $ recovering+            (constantDelay 5000 <> limitRetries 3)+            [const $ Handler $ \ Custom1 -> return shouldRetry]+            f+          (res :: Either Custom1 ()) @?= Right ()+++      , testCase "fails beyond policy using custom exceptions" $ do+          f <- mkFailN Custom1 3+          res <- try $ recovering+            (constantDelay 5000 <> limitRetries 2)+            [const $ Handler $ \ Custom1 -> return shouldRetry]+            f+          (res :: Either Custom1 ()) @?= Left Custom1+++      , testCase "does not recover from unhandled exceptions" $ do+          f <- mkFailN Custom2 2+          res <- try $ recovering+            (constantDelay 5000 <> limitRetries 5)+            [const $ Handler $ \ Custom1 -> return shouldRetry]+            f+          (res :: Either Custom2 ()) @?= Left Custom2+++      , testCase "recovers in presence of multiple handlers" $ do+          f <- mkFailN Custom2 2+          res <- try $ recovering+            (constantDelay 5000 <> limitRetries 5)+            [ const $ Handler $ \ Custom1 -> return shouldRetry+            , const $ Handler $ \ Custom2 -> return shouldRetry ]+            f+          (res :: Either Custom2 ()) @?= Right ()+++      , testCase "general exceptions catch specific ones" $ do+          f <- mkFailN Custom2 2+          res <- try $ recovering+            (constantDelay 5000 <> limitRetries 5)+            [ const $ Handler $ \ (_::SomeException) -> return shouldRetry ]+            f+          (res :: Either Custom2 ()) @?= Right ()+++      , testCase "(redundant) even general catchers don't go beyond policy" $ do+          f <- mkFailN Custom2 3+          res <- try $ recovering+            (constantDelay 5000 <> limitRetries 2)+            [ const $ Handler $ \ (_::SomeException) -> return shouldRetry ]+            f+          (res :: Either Custom2 ()) @?= Left Custom2+++      , testCase "rethrows in presence of failed exception casts" $ do+          f <- mkFailN Custom2 3+          final <- try $ do+            res <- try $ recovering+              (constantDelay 5000 <> limitRetries 2)+              [ const $ Handler $ \ (_::SomeException) -> return shouldRetry ]+              f+            (res :: Either Custom1 ()) @?= Left Custom1+          final @?= Left Custom2+      ]+  ]+++-------------------------------------------------------------------------------+monoidTests :: TestTree+monoidTests = testGroup "Policy is a monoid"+  [ testProperty "left identity" $ property $+      propIdentity (\p -> mempty <> p) id+  , testProperty "right identity" $ property $+      propIdentity (\p -> p <> mempty) id+  , testProperty "associativity" $ property $+      propAssociativity (\x y z -> x <> (y <> z)) (\x y z -> (x <> y) <> z)+  ]+  where+    propIdentity left right  = do+      retryStatus <- forAll genRetryStatus+      fixedDelay <- forAll (Gen.maybe (Gen.int (Range.linear 0 maxBound)))+      let calculateDelay _rs = fixedDelay+      let applyPolicy' f = getRetryPolicyM (f $ retryPolicy calculateDelay) retryStatus+          validRes = maybe True (>= 0)+      l <- liftIO $ applyPolicy' left+      r <- liftIO $ applyPolicy' right+      if validRes r && validRes l+        then l === r+        else return ()+    propAssociativity left right  = do+      retryStatus <- forAll genRetryStatus+      let genDelay = Gen.maybe (Gen.int (Range.linear 0 maxBound))+      delayA <- forAll genDelay+      delayB <- forAll genDelay+      delayC <- forAll genDelay+      let applyPolicy' f = liftIO $ getRetryPolicyM (f (retryPolicy (const delayA)) (retryPolicy (const delayB)) (retryPolicy (const delayC))) retryStatus+      res <- liftIO (liftA2 (==) (applyPolicy' left) (applyPolicy' right))+      assert res+++-------------------------------------------------------------------------------+retryStatusTests :: TestTree+retryStatusTests = testGroup "retry status"+  [ testCase "passes the correct retry status each time" $ do+      let policy = limitRetries 2 <> constantDelay 100+      rses <- gatherStatuses policy+      rsIterNumber <$> rses @?= [0, 1, 2]+      rsCumulativeDelay <$> rses @?= [0, 100, 200]+      rsPreviousDelay <$> rses @?= [Nothing, Just 100, Just 100]+  ]+++-------------------------------------------------------------------------------+policyTransformersTests :: TestTree+policyTransformersTests = testGroup "policy transformers"+  [ testProperty "always produces positive delay with positive constants (no rollover)" $ property $ do+      delay <- forAll (Gen.int (Range.linear 0 maxBound))+      let res = runIdentity (simulatePolicy 1000 (exponentialBackoff delay))+          delays = catMaybes (snd <$> res)+          mnDelay = if null delays+                      then Nothing+                      else Just (minimum delays)+      case mnDelay of+        Nothing -> return ()+        Just n -> do+          footnote (show n ++ " is not >= 0")+          HH.assert (n >= 0)+  , testProperty "positive, nonzero exponential backoff is always incrementing" $ property $ do+     delay <- forAll (Gen.int (Range.linear 1 maxBound))+     let res = runIdentity (simulatePolicy 1000 (limitRetriesByDelay maxBound (exponentialBackoff delay)))+         delays = catMaybes (snd <$> res)+     sort delays === delays+     length (group delays) === length delays+  ]+++-------------------------------------------------------------------------------+maskingStateTests :: TestTree+maskingStateTests = testGroup "masking state"+  [ testCase "shouldn't change masking state in a recovered action" $ do+      maskingState <- getMaskingState+      final <- try $ recovering def testHandlers $ const $ do+        maskingState' <- getMaskingState+        maskingState' @?= maskingState+        fail "Retrying..."+      assertBool+        ("Expected IOException but didn't get one")+        (isLeft (final :: Either IOException ()))++  , testCase "should mask asynchronous exceptions in exception handlers" $ do+      let checkMaskingStateHandlers =+            [ const $ Handler $ \(_ :: SomeException) -> do+                maskingState <- getMaskingState+                maskingState @?= MaskedInterruptible+                return shouldRetry+            ]+      final <- try $ recovering def checkMaskingStateHandlers $ const $ fail "Retrying..."+      assertBool+        ("Expected IOException but didn't get one")+        (isLeft (final :: Either IOException ()))+  ]+++-------------------------------------------------------------------------------+capDelayTests :: TestTree+capDelayTests = testGroup "capDelay"+  [ testProperty "respects limitRetries" $ property $ do+      retries <- forAll (Gen.int (Range.linear 1 100))+      cap <- forAll (Gen.int (Range.linear 1 maxBound))+      let policy = capDelay cap (limitRetries retries)+      let delays = runIdentity (simulatePolicy (retries + 1) policy)+      let Just lastDelay = lookup (retries - 1) delays+      let Just gaveUp = lookup retries delays+      let noDelay = 0+      lastDelay === Just noDelay+      gaveUp === Nothing+  , testProperty "does not allow any delays higher than the given delay" $ property $ do+      cap <- forAll (Gen.int (Range.linear 1 maxBound))+      baseDelay <- forAll (Gen.int (Range.linear 1 100))+      basePolicy <- forAllWith (const "RetryPolicy") (genScalingPolicy baseDelay)+      let policy = capDelay cap basePolicy+      let delays = catMaybes (snd <$> runIdentity (simulatePolicy 100 policy))+      let baddies = filter (> cap) delays+      baddies === []+  ]+++-------------------------------------------------------------------------------+-- | Generates policies that increase on each iteration+genScalingPolicy :: (Alternative m) => Int -> m (RetryPolicyM Identity)+genScalingPolicy baseDelay =+  (pure (exponentialBackoff baseDelay) <|> pure (fibonacciBackoff baseDelay))+++-------------------------------------------------------------------------------+limitRetriesByCumulativeDelayTests :: TestTree+limitRetriesByCumulativeDelayTests = testGroup "limitRetriesByCumulativeDelay"+  [ testProperty "never exceeds the given cumulative delay" $ property $ do+      baseDelay <- forAll (Gen.int (Range.linear 1 100))+      basePolicy <- forAllWith (const "RetryPolicy") (genScalingPolicy baseDelay)+      cumulativeDelayMax <- forAll (Gen.int (Range.linear 1 10000))+      let policy = limitRetriesByCumulativeDelay cumulativeDelayMax basePolicy+      let delays = catMaybes (snd <$> runIdentity (simulatePolicy 100 policy))+      footnoteShow delays+      let actualCumulativeDelay = sum delays+      footnote (show actualCumulativeDelay <> " <= " <> show cumulativeDelayMax)+      HH.assert (actualCumulativeDelay <= cumulativeDelayMax)++  ]++-------------------------------------------------------------------------------+quadraticDelayTests :: TestTree+quadraticDelayTests = testGroup "quadratic delay"+  [ testProperty "recovering test with quadratic retry delay" $ property $ do+      startTime <- liftIO getCurrentTime+      timeout <- forAll (Gen.int (Range.linear 0 15))+      retries <- forAll (Gen.int (Range.linear 0 8))+      res <- liftIO $ try $ recovering+        (exponentialBackoff timeout <> limitRetries retries)+        [const $ Handler (\(_::SomeException) -> return True)]+        (const $ throwM (userError "booo"))+      endTime <- liftIO getCurrentTime+      HH.assert (isLeftAnd isUserError res)+      let tmo = if retries > 0 then timeout * 2 ^ (retries - 1) else 0+      let ms' = ((fromInteger . toInteger $ tmo) / 1000000.0)+      HH.assert (diffUTCTime endTime startTime >= ms')+  ]++-------------------------------------------------------------------------------+isLeftAnd :: (a -> Bool) -> Either a b -> Bool+isLeftAnd f ei = case ei of+  Left v -> f v+  _      -> False++testHandlers :: [a -> Handler IO Bool]+testHandlers = [const $ Handler (\(_::SomeException) -> return shouldRetry)]+++data Custom1 = Custom1 deriving (Eq,Show,Read,Ord,Typeable)+data Custom2 = Custom2 deriving (Eq,Show,Read,Ord,Typeable)+++instance Exception Custom1+instance Exception Custom2+++-------------------------------------------------------------------------------+genRetryStatus :: MonadGen m => m RetryStatus+genRetryStatus = do+  n <- Gen.int (Range.linear 0 maxBound)+  d <- Gen.int (Range.linear 0 maxBound)+  l <- Gen.maybe (Gen.int (Range.linear 0 d))+  return $ defaultRetryStatus { rsIterNumber = n+                              , rsCumulativeDelay = d+                              , rsPreviousDelay = l}+++-------------------------------------------------------------------------------+++-------------------------------------------------------------------------------+-- | Create an action that will fail exactly N times with the given+-- exception and will then return () in any subsequent calls.+mkFailN :: (Exception e) => e -> Int -> IO (s -> IO ())+mkFailN e n = do+    r <- newIORef 0+    return $ const $ do+      old <- atomicModifyIORef' r $ \ old -> (old+1, old)+      case old >= n of+        True  -> return ()+        False -> throwM e+++-------------------------------------------------------------------------------+gatherStatuses+    :: MonadIO m+    => RetryPolicyM (WriterT [RetryStatus] m)+    -> m [RetryStatus]+gatherStatuses policy = execWriterT $+  retrying policy (\_ _ -> return shouldRetry)+                  (\rs -> tell [rs])+++-------------------------------------------------------------------------------+-- | Just makes things a bit easier to follow instead of a magic value+-- of @return True@+shouldRetry :: Bool+shouldRetry = True
− test/main.hs
@@ -1,2 +0,0 @@-{-# OPTIONS_GHC -F -pgmF hspec-discover #-}-