packages feed

lazy-cache-0.2.0.0: src/System/Cache/Impl/Ghc.hs

{-# LANGUAGE RecursiveDo #-}
-- | Caching based on the GHC heap object properties and  relies on lazyness implementation
-- in the GHC RTS. This approach does it's best effort for avoiding the duplicated work, 
-- however it the pathological cases it's possible that two theads will run the computation
-- on the same input concurrently. We have not observed this case in practice, but in the
-- case if you need strict guarantees on this property you should use "System.Cache.Impl.MVar"
-- instead.
--
-- This is battle-tested implementation. That fits all the properties that are required by the
-- interface.
--
module System.Cache.Impl.Ghc
  ( new
  ) where

import Control.Exception
import Data.Hashable
import Data.HashPSQ as PSQ
import Data.IORef
import Data.Tuple
import Control.Monad
import System.Clock.Seconds

import System.Cache.Internal.Interface

-- | We need our internal values to be even more lazy, in order
-- to prevent early evaluation and deadlock.
data Lazy a = Lazy { getLazy :: a}

-- | Creates new cache Handle. Keeps priotity queue from @psqueues@ package inside.
--
-- Properties:
--
--  * if multiple threads are running an IO action with the same input
--    concurrently then only one (with the best effort) will run, all
--    the rest will wait for that action and will either return a value,
--    or throw an exception depending on the the result of the first one
--  * storage is cleared only during access, it will remove redundant work
--    but may lead to the situation when cached values are stored in memory
--    for a longer period that it was expected
--  * psqueue structure uses both 'Hashable' and 'Ord' constraints and is
--    not vulnerable to the hash collision attacks.
--
new
  :: (Show a, Hashable a, Ord a)
  => Config
  -> IO (Handle a b)
new Config {..} = do
  ref <- newIORef PSQ.empty
  pure $ Handle
    { requestOrInternal = \tm k f -> mdo
      mbox <-
        atomicModifyIORef ref
        $ swap
        . PSQ.alter
            (\case
              Just (p, ~v) | p >= diffTimeSpec tm configLongestAge ->
                (Just v, Just (p, v))
              _ -> (Nothing, Just (tm, Lazy eresult))
            )
            k
      eresult <- try $ maybe
        (f k)
        (\r -> do
          evaluate (getLazy r) >>= \case
            Left s -> throwIO s
            Right x -> pure x
        )
        mbox
      result <- case eresult of
        Left (s::SomeException) -> do
          atomicModifyIORef' ref $ \v -> (PSQ.delete k v,  ())
          throwIO s
        Right x -> pure x
      atomicModifyIORef' ref $ swap . PSQ.alterMin
        (\case
          Nothing -> ((), Nothing)
          Just (kk, p, v)
            | p < diffTimeSpec tm configLongestAge -> ((), Nothing)
            | otherwise -> ((), Just (kk, p, v))
        )
      pure result
    , remove = \k -> do
       void $ atomicModifyIORef ref $ swap . PSQ.alter (const ((), Nothing)) k
    , getClockTime = getTime configClock
    }