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
}