packages feed

cached-io 1.3.0.0 → 1.3.1.0

raw patch · 4 files changed

+229/−99 lines, 4 filesdep +tastydep +tasty-hunitdep −transformersdep ~timePVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: tasty, tasty-hunit

Dependencies removed: transformers

Dependency ranges changed: time

API changes (from Hackage documentation)

+ Control.Concurrent.CachedIO: cachedSTM :: (MonadIO m, MonadCatch m) => NominalDiffTime -> m a -> STM (Cached m a)
+ Control.Concurrent.CachedIO: cachedSTM' :: (MonadIO m, MonadCatch m) => NominalDiffTime -> (Maybe (UTCTime, a) -> m a) -> STM (Cached m a)
+ Control.Concurrent.CachedIO: cachedSTMWith :: (MonadIO m, MonadCatch m) => (UTCTime -> UTCTime -> Bool) -> m a -> STM (Cached m a)
+ Control.Concurrent.CachedIO: cachedSTMWith' :: (MonadIO m, MonadCatch m) => (UTCTime -> UTCTime -> Bool) -> (Maybe (UTCTime, a) -> m a) -> STM (Cached m a)
- Control.Concurrent.CachedIO: Cached :: m a -> Cached m a
+ Control.Concurrent.CachedIO: Cached :: m a -> Cached (m :: Type -> Type) a
- Control.Concurrent.CachedIO: [runCached] :: Cached m a -> m a
+ Control.Concurrent.CachedIO: [runCached] :: Cached (m :: Type -> Type) a -> m a
- Control.Concurrent.CachedIO: newtype Cached m a
+ Control.Concurrent.CachedIO: newtype Cached (m :: Type -> Type) a

Files

CHANGELOG.md view
@@ -1,5 +1,11 @@ # Revision history for cached-io +## 1.3.1.0++- Add `cachedSTM` versions of all bindings.+- Correctly transition the internal state to `Initializing` when filling the cache for the first time.+- Remove unused `transformer` dependency+ ## 1.3.0.0  - **Breaking** Caching functions previously returned `m (t a)`, but it was easy to accidentally use `join` when `m` and `t` were the same monad (eg. `IO (IO a)`), and not get any caching at all. These functions now use a `Cached` newtype for `t a` to make it more difficult to misuse.
cached-io.cabal view
@@ -1,18 +1,27 @@-cabal-version:      2.2-name:               cached-io-version:            1.3.0.0-synopsis:           A simple library to cache IO actions+cabal-version:   2.2+name:            cached-io+version:         1.3.1.0+synopsis:        A simple library to cache IO actions description:   Provides functions that convert an IO action into a cached one by storing the   original result for a period of time, or until some condition is met. -license:            Apache-2.0-license-file:       LICENSE-author:             SumAll, Inc.-maintainer:         Bellroy Tech Team <haskell@bellroy.com>-category:           Development-build-type:         Simple-tested-with:        GHC ==8.8.4 || ==8.10.7 || ==9.0.2 || ==9.2.4 || ==9.4.5 || ==9.6.2 || ==9.8.1+license:         Apache-2.0+license-file:    LICENSE+author:          SumAll, Inc.+maintainer:      Bellroy Tech Team <haskell@bellroy.com>+category:        Development+build-type:      Simple+tested-with:+  GHC ==8.10.7+   || ==9.0.2+   || ==9.2.4+   || ==9.4.5+   || ==9.6.6+   || ==9.8.2+   || ==9.10.1+   || ==9.12.1+ extra-doc-files:   CHANGELOG.md   README.md@@ -22,16 +31,15 @@   location: https://github.com/bellroy/haskell-cached-io.git  common deps-  build-depends: base >=4.13.0.0 && <4.20+  build-depends: base >=4.13.0.0 && <4.22  library   import:           deps   exposed-modules:  Control.Concurrent.CachedIO   build-depends:-    , exceptions    >=0.10.4  && <0.11-    , stm           >=2.5     && <2.6-    , time          >=1.9.3   && <1.15-    , transformers  >=0.5.6.2 && <0.7+    , exceptions  >=0.10.4 && <0.11+    , stm         >=2.5    && <2.6+    , time        >=1.9.3  && <1.16    hs-source-dirs:   src/   default-language: Haskell2010@@ -45,8 +53,9 @@   default:     False   description: compile with -Werror to make warnings fatal -executable test-cachedIO+test-suite test-cachedIO   import:           deps+  type:             exitcode-stdio-1.0   main-is:          test-cachedIO.hs   build-depends:    cached-io   hs-source-dirs:   test/@@ -57,3 +66,7 @@    if flag(developer)     ghc-options: -Werror++  build-depends:+    , tasty        ^>=1.5+    , tasty-hunit  ^>=0.10.0.3
src/Control/Concurrent/CachedIO.hs view
@@ -11,28 +11,46 @@ -- -- * before 10 minutes have passed, it returns the stored value. -- * after 10 minutes have passed, it calls @downloadData@ and stores the--- result again.----module Control.Concurrent.CachedIO (-    Cached(..),+--   result again.+-- * @downloadData@ will not be called if a different thread is already calling+--   it. In that case, the stored value will be returned in the meantime.+module Control.Concurrent.CachedIO+  ( Cached (..),++    -- * IO     cachedIO,     cachedIOWith,     cachedIO',-    cachedIOWith'-    ) where+    cachedIOWith', -import Control.Concurrent.STM (atomically, newTVar, readTVar, writeTVar, retry, TVar)+    -- * STM+    -- $stm+    cachedSTM,+    cachedSTMWith,+    cachedSTM',+    cachedSTMWith',+  )+where++import Control.Concurrent.STM+  ( STM,+    atomically,+    newTVar,+    readTVar,+    retry,+    writeTVar,+  ) import Control.Monad (join) import Control.Monad.Catch (MonadCatch, onException)-import Control.Monad.IO.Class (liftIO, MonadIO)-import Data.Time.Clock (NominalDiffTime, addUTCTime, getCurrentTime, UTCTime)+import Control.Monad.IO.Class (MonadIO, liftIO)+import Data.Time.Clock (NominalDiffTime, UTCTime, addUTCTime, getCurrentTime)  -- | A cached IO action in some monad @m@. Use 'runCached' to extract the action when you want to query it. -- -- Note that using 'Control.Monad.join' when the cached action and the outer monad are the same will ignore caching. newtype Cached m a = Cached {runCached :: m a} -data State a  = Uninitialized | Initializing | Updating a | Fresh UTCTime a+data State a = Uninitialized | Initializing | Updating a | Fresh UTCTime a  -- | Cache an IO action, producing a version of this IO action that is cached -- for 'interval' seconds. The cache begins uninitialized.@@ -40,11 +58,14 @@ -- The outer IO is responsible for setting up the cache. Use the inner one to -- either get the cached value or refresh, if the cache is older than 'interval' -- seconds.-cachedIO :: (MonadIO m, MonadIO t, MonadCatch t)-         => NominalDiffTime -- ^ Number of seconds before refreshing cache-         -> t a             -- ^ IO action to cache-         -> m (Cached t a)-cachedIO interval = cachedIOWith (secondsPassed interval)+cachedIO ::+  (MonadIO m, MonadIO t, MonadCatch t) =>+  -- | Number of seconds before refreshing cache+  NominalDiffTime ->+  -- | IO action to cache+  t a ->+  m (Cached t a)+cachedIO = cachedIOWith . secondsPassed  -- | Cache an IO action, producing a version of this IO action that is cached -- for 'interval' seconds. The cache begins uninitialized.@@ -52,66 +73,122 @@ -- The outer IO is responsible for setting up the cache. Use the inner one to -- either get the cached value or refresh, if the cache is older than 'interval' -- seconds.-cachedIO' :: (MonadIO m, MonadIO t, MonadCatch t)-          => NominalDiffTime -- ^ Number of seconds before refreshing cache-          -> (Maybe (UTCTime, a) -> t a) -- ^ action to cache. The stale value and its refresh date-          -- are passed so that the action can perform external staleness checks-          -> m (Cached t a)-cachedIO' interval = cachedIOWith' (secondsPassed interval)---- | Check if @starting time@ + @seconds@ is after @end time@-secondsPassed :: NominalDiffTime  -- ^ Seconds-               -> UTCTime         -- ^ Start time-               -> UTCTime         -- ^ End time-               -> Bool-secondsPassed interval start end = addUTCTime interval start > end+cachedIO' ::+  (MonadIO m, MonadIO t, MonadCatch t) =>+  -- | Number of seconds before refreshing cache+  NominalDiffTime ->+  -- | action to cache. The stale value and its refresh date+  -- are passed so that the action can perform external staleness checks+  (Maybe (UTCTime, a) -> t a) ->+  m (Cached t a)+cachedIO' = cachedIOWith' . secondsPassed  -- | Cache an IO action, The cache begins uninitialized. -- -- The outer IO is responsible for setting up the cache. Use the inner one to -- either get the cached value or refresh-cachedIOWith-    :: (MonadIO m, MonadIO t, MonadCatch t)-    => (UTCTime -> UTCTime -> Bool) -- ^ Test function:-    --   If 'isCacheStillFresh' 'lastUpdated' 'now' returns 'True'-    --   the cache is considered still fresh and returns the cached IO action-    -> t a -- ^ action to cache.-    -> m (Cached t a)-cachedIOWith f io = cachedIOWith' f (const io)+cachedIOWith ::+  (MonadIO m, MonadIO t, MonadCatch t) =>+  -- | Test function:+  -- If @isCacheStillFresh lastUpdated now@ returns 'True',+  -- the cache is considered still fresh and returns the cached IO action+  (UTCTime -> UTCTime -> Bool) ->+  -- | Action to cache.+  t a ->+  m (Cached t a)+cachedIOWith f = cachedIOWith' f . const --- | Cache an IO action, The cache begins uninitialized.+-- | Cache an IO action; the cache begins uninitialized. -- -- The outer IO is responsible for setting up the cache. Use the inner one to -- either get the cached value or refresh-cachedIOWith'-    :: (MonadIO m, MonadIO t, MonadCatch t)-    => (UTCTime -> UTCTime -> Bool) -- ^ Test function:-    --   If 'isCacheStillFresh' 'lastUpdated' 'now' returns 'True'-    --   the cache is considered still fresh and returns the cached IO action-    -> (Maybe (UTCTime, a) -> t a) -- ^ action to cache. The stale value and its refresh date-    -- are passed so that the action can perform external staleness checks-    -> m (Cached t a)-cachedIOWith' isCacheStillFresh io = do-  cachedT <- liftIO (atomically (newTVar Uninitialized))+cachedIOWith' ::+  (MonadIO m, MonadIO t, MonadCatch t) =>+  -- | Test function:+  -- If 'isCacheStillFresh' 'lastUpdated' 'now' returns 'True'+  -- the cache is considered still fresh and returns the cached IO action+  (UTCTime -> UTCTime -> Bool) ->+  -- | Action to cache. The stale value and its refresh date+  -- are passed so that the action can perform external staleness checks+  (Maybe (UTCTime, a) -> t a) ->+  m (Cached t a)+cachedIOWith' isCacheStillFresh refreshAction =+  liftIO . atomically $ cachedSTMWith' isCacheStillFresh refreshAction++-- $stm+--+-- The following actions are the exactly same as the 'cachedIO' versions, except+-- that they do not leave the 'STM' monad during construction.++-- | Set up a cached IO action in a transaction; producing a version of this IO+-- action that is cached for 'interval' seconds. The cache begins uninitialized.+cachedSTM ::+  (MonadIO m, MonadCatch m) =>+  -- | Number of seconds before refreshing cache+  NominalDiffTime ->+  -- | IO action to cache+  m a ->+  STM (Cached m a)+cachedSTM = cachedSTMWith . secondsPassed++-- | Set up a cached IO action in a transaction; producing a version of this IO+-- action that is cached for 'interval' seconds. The cache begins uninitialized.+cachedSTM' ::+  (MonadIO m, MonadCatch m) =>+  -- | Number of seconds before refreshing cache+  NominalDiffTime ->+  -- | action to cache. The stale value and its refresh date+  -- are passed so that the action can perform external staleness checks+  (Maybe (UTCTime, a) -> m a) ->+  STM (Cached m a)+cachedSTM' = cachedSTMWith' . secondsPassed++-- | Set up a cached IO action in a transaction; The cache begins uninitialized.+cachedSTMWith ::+  (MonadIO m, MonadCatch m) =>+  -- | Test function:+  -- If @isCacheStillFresh lastUpdated now@ returns 'True',+  -- the cache is considered still fresh and returns the cached IO action+  (UTCTime -> UTCTime -> Bool) ->+  -- | Action to cache.+  m a ->+  STM (Cached m a)+cachedSTMWith f = cachedSTMWith' f . const++-- | Set up a cached IO action in a transaction; the cache begins uninitialized.+cachedSTMWith' ::+  (MonadIO m, MonadCatch m) =>+  -- | Test function:+  -- If @'isCacheStillFresh' lastUpdated now@ returns 'True'+  -- the cache is considered still fresh and returns the cached IO action+  (UTCTime -> UTCTime -> Bool) ->+  -- | Action to cache. The stale value and its refresh date+  -- are passed so that the action can perform external staleness checks+  (Maybe (UTCTime, a) -> m a) ->+  STM (Cached m a)+cachedSTMWith' isCacheStillFresh refreshAction = do+  cachedT <- newTVar Uninitialized   pure . Cached $ do     now <- liftIO getCurrentTime     join . liftIO . atomically $ do       cached <- readTVar cachedT       case cached of         previousState@(Fresh lastUpdated value)-        -- There's data in the cache and it's recent. Just return.-          | isCacheStillFresh lastUpdated now -> return (return value)-        -- There's data in the cache, but it's stale. Update the cache state-        -- to prevent a second thread from also executing the action. The second-        -- thread will get the stale data instead.+          -- There's data in the cache and it's recent. Just return.+          | isCacheStillFresh lastUpdated now -> pure (pure value)+          -- There's data in the cache, but it's stale. Update the cache state+          -- to prevent a second thread from also executing the action. The second+          -- thread will get the stale data instead.           | otherwise -> do-            writeTVar cachedT (Updating value)-            pure (refreshCache previousState cachedT)+              writeTVar cachedT (Updating value)+              pure (refreshCache previousState cachedT)         -- Another thread is already updating the cache, just return the stale value         Updating value -> pure (pure value)         -- The cache is uninitialized. Mark the cache as initializing to block other         -- threads. Initialize and return.-        Uninitialized -> pure (refreshCache Uninitialized cachedT)+        Uninitialized -> do+          writeTVar cachedT Initializing+          pure (refreshCache Uninitialized cachedT)         -- The cache is uninitialized and another thread is already attempting to         -- initialize it. Block.         Initializing -> retry@@ -119,11 +196,22 @@     refreshCache previousState cachedT = do       let previous = case previousState of             Fresh lastUpdated value -> Just (lastUpdated, value)-            _                       -> Nothing-      newValue <- io previous `onException` restoreState previousState cachedT-      now <- liftIO getCurrentTime-      liftIO (atomically (writeTVar cachedT (Fresh now newValue)))-      liftIO (return newValue)+            _ -> Nothing+      newValue <-+        refreshAction previous+          `onException` liftIO (atomically (writeTVar cachedT previousState))+      liftIO $ do+        now <- getCurrentTime+        atomically (writeTVar cachedT (Fresh now newValue))+        pure newValue -restoreState :: (MonadIO m) => State a -> TVar (State a) -> m ()-restoreState previousState cachedT = liftIO (atomically (writeTVar cachedT previousState))+-- | Check if @starting time@ + @seconds@ is after @end time@+secondsPassed ::+  -- | Seconds+  NominalDiffTime ->+  -- | Start time+  UTCTime ->+  -- | End time+  UTCTime ->+  Bool+secondsPassed interval start end = addUTCTime interval start > end
test/test-cachedIO.hs view
@@ -1,25 +1,48 @@-module Main (-    main-    ) where+module Main (main) where -import Control.Concurrent.CachedIO (cachedIO, Cached(..))-import Data.List (isInfixOf)+import Control.Concurrent (forkIO, threadDelay)+import Control.Concurrent.CachedIO (Cached (..), cachedIO)+import Data.Functor (void)+import Data.IORef (IORef, atomicModifyIORef', newIORef, readIORef)+import Test.Tasty (TestTree, defaultMain, testGroup)+import Test.Tasty.HUnit (testCase, (@?=)) -crawlTheInternet :: IO [String]-crawlTheInternet = do-    putStrLn "Scanning pages.."-    putStrLn "Parsing HTML.."-    putStrLn "Following links.."-    return ["website about Haskell", "website about Ruby", "slashdot.org",-            "The Monad.Reader", "haskellwiki"]+tests :: TestTree+tests =+  testGroup+    "Control.Concurrent.CachedIO"+    [ testCase "Action is not called until first use" $ do+        ref <- newIORef 0+        Cached {} <- cachedIO (5 * 60) $ increment ref+        count <- readIORef ref+        count @?= 0,+      testCase "Action is not called if value is fresh" $ do+        ref <- newIORef 0+        Cached action <- cachedIO (5 * 60) $ increment ref+        void action+        void action+        count <- readIORef ref+        count @?= 1,+      testCase "Action is not called if cache is initializing" $ do+        ref <- newIORef 0+        Cached action <- cachedIO (5 * 60) $ incrementSlow ref+        void . forkIO $ void action+        void action+        count <- readIORef ref+        count @?= 1+    ] -searchEngine :: String -> Cached IO [String] -> IO [String]-searchEngine query internet = do-    pages <- runCached internet-    return $ filter (query `isInfixOf`) pages+increment :: IORef Int -> IO Int+increment ref = atomicModifyIORef' ref (\i -> (succ i, i)) +incrementSlow :: IORef Int -> IO Int+incrementSlow ref = do+  res <- atomicModifyIORef' ref (\i -> (succ i, i))+  -- waiting AFTER the increase will show that Initialized is necessary+  -- because forking a thread takes enough time that the "action >> readIORef"+  -- are done before the forked thread has the chance to increment the counter.+  threadDelay $ 100 * 1000 -- 100 ms+  pure res+ main :: IO ()-main = do-    cachedInternet <- cachedIO 600 crawlTheInternet   -- 10 minute cache-    print =<< searchEngine "Haskell" cachedInternet-    print =<< searchEngine "C#" cachedInternet+main = defaultMain tests