packages feed

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

-- | Safer cache version that is implemented totally
-- on the user level. Using 'MVar' as a synchronization
-- primitive.
--
-- While this implementation is slower in general case it may
-- be more resilient to changes in GHC and RTS. This implementation
-- is mostly used in order to sanity check the GHC one, and in order
-- to be able to switch to this one without program recompilation.
--
-- However if you need strict guarantees that the only one action with
-- the same input is run concurrently you should prefer this implementation.
module System.Cache.Impl.MVar
  ( new
  ) where

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

import System.Cache.Internal.Interface

-- | Create new cache handle. Keeps priority queue from psqueues package inside.
--
-- Properties:
--
--  * if multiple threads are running an IO action with the same input
--    concurrently then only one 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 cfg@Config {..} = do
  ref <- newIORef PSQ.empty
  pure $ Handle
    { requestOrInternal = \tm k f -> do
        queue <- readIORef ref
        result
          <- case PSQ.lookup k queue of
               Nothing -> insertElement cfg ref tm k f
               Just (p, (r, lock)) -> do
                 readIORef r >>= \case
                   Nothing
                     -> updateLock cfg ref lock r tm k f
                   Just v
                     | p >= tm - configLongestAge -> pure v
                     | otherwise -> updateLock cfg ref lock r tm k f
        atomicModifyIORef' ref $ swap . PSQ.alterMin
                (\case
                  Nothing -> ((), Nothing)
                  Just (kk, p, v)
                    | p < 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
    }

-- | There is no value in the queue, but someone may already trying to create a lock,
-- so we need to register a lock, verifying that it was registered concurrently.
insertElement :: (Hashable k, Ord k)
  => Config
  -> IORef (HashPSQ k Seconds (IORef (Maybe z), MVar (Maybe (Seconds, z))))
  -> Seconds
  -> k
  -> (k -> IO z)
  -> IO z
insertElement cfg ref tm k f = newEmptyMVar >>= \x -> go x `onException` (tryPutMVar x Nothing) where
  go our_lock = do
    result_box <- newIORef Nothing
    update_result
      <- atomicModifyIORef ref $ swap .
        PSQ.alter
          (\case
             Just x@(_, their_lock) -> (Just their_lock, Just x)
             Nothing -> (Nothing, Just (tm, (result_box, our_lock)))
          ) k
    case update_result of
      Just (r, their_lock)
        -> updateLock cfg ref their_lock r tm k f -- Someone else managed to insert the lock, first.
      Nothing -> do
        -- We are holding a lock, so we need to evaluate value and register it
        value <- f k
        putMVar our_lock (Just (tm, value))
        writeIORef result_box (Just value)
        pure value

updateLock :: (Hashable k, Ord k)
  => Config
  -> IORef (HashPSQ k Seconds (IORef (Maybe z), MVar (Maybe (Seconds, z))))
  -> MVar (Maybe (Seconds, z))
  -> IORef (Maybe z)
  -> Seconds 
  -> k
  -> (k -> IO z)
  -> IO z
updateLock Config{..} ref lock inner tm k f  = modifyMVar lock $ \case
  Just x@(p, v) -- Result exists and is valid, we can just return it
    | p >= tm - configLongestAge -> pure (Just x, v)
  _ -> do -- There is no result or it's outdated, we can update it.
    value <- f k
    writeIORef inner (Just value)
    atomicModifyIORef ref (\queue -> (PSQ.insert k tm (inner,lock) queue, ()))
    pure (Just (tm, value), value)