cached-io-1.1.0.0: src/Control/Concurrent/CachedIO.hs
module Control.Concurrent.CachedIO (
cachedIO,
cachedIOWith
) where
import Control.Concurrent.STM (atomically, newTVar, readTVar, writeTVar, retry)
import Control.Monad (join)
import Control.Monad.IO.Class (liftIO, MonadIO)
import Data.Time.Clock (NominalDiffTime, addUTCTime, getCurrentTime, UTCTime)
data State = Uninitialized | Initializing
-- | Cache an IO action, producing a version of this IO action that is cached
-- for 'interval' seconds. 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, if the cache is older than 'interval'
-- seconds.
cachedIO :: (MonadIO m)
=> NominalDiffTime -- ^ Number of seconds before refreshing cache
-> m a -- ^ IO action to cache
-> m (m 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
-- | 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)
=> (UTCTime -> UTCTime -> Bool) -- ^ Test function:
-- If 'isCacheStillFresh' 'lastUpdated' 'now' returns 'True'
-- the cache is considered still fresh and returns the cached IO action
-> m a -- ^ action to cache
-> m (m a)
cachedIOWith isCacheStillFresh io = do
initTime <- liftIO getCurrentTime
cachedT <- liftIO (atomically (newTVar (initTime, Left Uninitialized)))
return $ do
now <- liftIO getCurrentTime
join . liftIO . atomically $ do
cached <- readTVar cachedT
case cached of
-- There's data in the cache and it's recent. Just return.
(lastUpdated, Right value) | isCacheStillFresh lastUpdated now ->
return (return value)
-- There's data in the cache, but it's stale. Update the cache timestamp
-- to prevent a second thread from also executing the IO action. The second
-- thread would get the stale data instead. Refresh the cache and return.
(_, Right value) -> do
writeTVar cachedT (now, Right value)
return $ refreshCache now cachedT
-- The cache is uninitialized. Mark the cache as initializing to block other
-- threads. Initialize and return.
(_, Left Uninitialized) -> do
writeTVar cachedT (now, Left Initializing)
return $ refreshCache now cachedT
-- The cache is uninitialized and another thread is already attempting to
-- initialize it. Block.
(_, Left Initializing) -> retry
where
refreshCache now cachedT = do
newValue <- io
liftIO (atomically (writeTVar cachedT (now, Right newValue)))
liftIO (return newValue)