packages feed

hackage-server-0.5.0: Distribution/Server/Framework/Cache.hs

module Distribution.Server.Framework.Cache (
    AsyncCache,
    newAsyncCacheNF,
    newAsyncCacheWHNF,
    AsyncCachePolicy(..),
    defaultAsyncCachePolicy,
    readAsyncCache,
    prodAsyncCache,
    syncAsyncCache,
    
    AsyncUpdate,
    newAsyncUpdate,
    asyncUpdate,
  ) where

import Control.Concurrent (forkIO, threadDelay)
import Control.Concurrent.STM
import Control.Exception
import Control.Monad
import Control.Monad.Trans (MonadIO(liftIO))
import Control.DeepSeq (NFData, rnf)

import Distribution.Server.Framework.Logging
import qualified Distribution.Verbosity as Verbosity

import Prelude hiding (catch)


-- | An in-memory cache with asynchronous updates.
--
-- Cache reads never block but may get stale results. So it is suitable for
-- cases where updates may be expensive, but where having slightly out of
-- date results is ok.
--
-- The cache is made with the action that calculates the new value.
-- So cache updates just prod the cache to recalculate.
--
-- * Note: this action is executed synchronously, it is the evaluation of
--   the result of this action that is performed asynchronously. The cache
--   is only actually updated when the evaluation of this result is complete.
--
data AsyncCache a = AsyncCache !(AsyncVar a) (IO a)

data AsyncCachePolicy = AsyncCachePolicy {
       -- | To reduce the cost of updating caches when there are lots of
       -- updates going on, we simply wait, that way more updates queue up
       -- and since we only take the last update, then we save computation.
       asyncCacheUpdateDelay :: !Int,

       -- | Usually we want to force the initial value synchronously but some
       -- apps may prefer to let that happen asynchronously (and in parallel)
       -- to speed up app initialisation. But in that case the app has to use
       -- 'syncAsyncCache' to be sure the cache is ready.
       asyncCacheSyncInit    :: !Bool,

       asyncCacheLogVerbosity:: !Verbosity,
       asyncCacheName        :: String
     }

defaultAsyncCachePolicy :: AsyncCachePolicy
defaultAsyncCachePolicy = AsyncCachePolicy 0 True Verbosity.normal "(unnamed)"

newAsyncCacheWHNF :: IO a -> AsyncCachePolicy -> IO (AsyncCache a)
newAsyncCacheWHNF = newAsyncCache (\x -> seq x ())

newAsyncCacheNF :: NFData a => IO a -> AsyncCachePolicy -> IO (AsyncCache a)
newAsyncCacheNF = newAsyncCache rnf

newAsyncCache :: (a -> ()) -> IO a -> AsyncCachePolicy -> IO (AsyncCache a)
newAsyncCache eval update (AsyncCachePolicy delay syncForce verbosity logname) = do
  x <- update
  avar <- newAsyncVar delay syncForce verbosity logname eval x
  return (AsyncCache avar update)

readAsyncCache :: MonadIO m => AsyncCache a -> m a
readAsyncCache (AsyncCache avar _) = liftIO $ readAsyncVar avar

prodAsyncCache :: MonadIO m => AsyncCache a -> m ()
prodAsyncCache (AsyncCache avar update) = liftIO $ update >>= writeAsyncVar avar

-- | Only needed if you use asynchronous initialisation
-- (i.e. ''asyncCacheSyncInit' = False@). Waits until the value in the cache
-- has been evaluated. It has no effect later on since the async cache always
-- has a value available (albeit perhaps a stale one).
syncAsyncCache :: NFData a => AsyncCache a -> IO ()
syncAsyncCache c = readAsyncCache c >>= evaluate . rnf >> return ()


-------------------------------------------------
-- A mutable variable with asynchronous updates
--

data AsyncVar state = AsyncVar !(TChan state)
                               !(TVar (Either SomeException state))

newAsyncVar :: Int -> Bool -> Verbosity -> String
            -> (state -> ()) -> state -> IO (AsyncVar state)
newAsyncVar delay syncForce verbosity logname force initial = do

    inChan <- atomically newTChan
    outVar <- atomically (newTVar (Right initial))

    if syncForce
      then logTiming verbosity ("Cache '" ++ logname ++ "' initialised") $
             evaluate (force initial)
      else atomically (writeTChan inChan initial)

    let loop = do

          when (delay > 0) (threadDelay delay)

          avail   <- readAllAvailable inChan
          -- We have a series of new values.
          -- We want the last one, skipping all intermediate updates.
          let value = last avail

          logTiming verbosity ("Cache '" ++ logname ++ "' updated") $ do
            res <- try $ evaluate (force value `seq` value)
            atomically (writeTVar outVar res)

          loop

    void $ forkIO loop
    return (AsyncVar inChan outVar)
  where
    -- get a list of all the input states currently queued
    readAllAvailable chan =
      atomically $ do
        x <- readTChan chan -- will block if queue is empty
        readAll [x]         -- will never block, just gets what's available
      where
        readAll xs = do
          empty <- isEmptyTChan chan
          if empty
            then return (reverse xs)
            else do x <- readTChan chan
                    readAll (x:xs)

readAsyncVar :: AsyncVar state -> IO state
readAsyncVar (AsyncVar _ outVar) =
    atomically (readTVar outVar) >>= either throw return

writeAsyncVar :: AsyncVar state -> state -> IO ()
writeAsyncVar (AsyncVar inChan _) value =
    atomically (writeTChan inChan value)


-----------------------------------------------------------------
-- A mechanism for async updates with multiple update prevention
--

newtype AsyncUpdate = AsyncUpdate (TChan (IO ()))

newAsyncUpdate :: Int -> Verbosity -> String -> IO AsyncUpdate
newAsyncUpdate delay verbosity logname = do

    inChan <- atomically newTChan

    let loop = do

          when (delay > 0) (threadDelay delay)

          avail   <- readAllAvailable inChan
          -- We have a series of new actions.
          -- We want the last one, skipping all intermediate updates.
          let action = last avail

          logTiming verbosity (logname ++ " updated") $
            action `catch` \e ->
               loginfo verbosity $ "Exception in update " ++ logname ++ ": "
                                ++ show (e :: SomeException)

          loop

    void $ forkIO loop
    return (AsyncUpdate inChan)
  where
    -- get a list of all the input states currently queued
    readAllAvailable chan =
      atomically $ do
        x <- readTChan chan -- will block if queue is empty
        readAll [x]         -- will never block, just gets what's available
      where
        readAll xs = do
          empty <- isEmptyTChan chan
          if empty
            then return (reverse xs)
            else do x <- readTChan chan
                    readAll (x:xs)

asyncUpdate :: AsyncUpdate -> IO () -> IO ()
asyncUpdate (AsyncUpdate inChan) action =
    atomically (writeTChan inChan action)