{-| Module : PostgREST.Cache.Sieve Description : PostgREST cache implementation based on Sieve algorithm. This module provides implementation of a mutable cache on Sieve algorithm. -} {-# LANGUAGE DataKinds #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE PolyKinds #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecursiveDo #-} {-# LANGUAGE StrictData #-} {-# LANGUAGE TupleSections #-} module PostgREST.Cache.Sieve ( Cache , CacheConfig (..) , Discard (..) , alwaysValid , cache , cacheIO , cached ) where import Control.Concurrent.STM import Control.Monad.Extra (whileM) import Data.Some import qualified Focus as F import Protolude hiding (elem, head) import qualified StmHamt.SizedHamt as SH data ListNode k v (b :: Bool) = ListNode { nextPtr :: NodePtr k v, prevNextPtrPtr :: NodePtrPtr k v, elem :: NodeElem k v b } data NodeElem :: Type -> Type -> Bool -> Type where Head :: { entries :: SH.SizedHamt (HamtEntry k v), finger :: NodePtrPtr k v } -> NodeElem k v False Entry :: Hashable k => { visited :: TVar Bool, ekey :: k, entryValue :: v } -> NodeElem k v True type HamtEntry k v = ListNode k v True type AnyNode k v = Some (ListNode k v) type NodePtr k v = TVar (AnyNode k v) type NodePtrPtr k v = TVar (NodePtr k v) data Discard m v = Refresh (m ()) | Invalid (m v) data Cache m k v = (MonadIO m, Hashable k) => Cache (ListNode k v False) (CacheConfig m k v) data CacheConfig m k v = CacheConfig { maxSize :: STM Int, load :: k -> m v, requestListener :: Bool -> m (), evictionListener :: k -> v -> m (), validator :: m (k -> v -> Maybe (Discard m v)) } alwaysValid :: Applicative m => m (k -> v -> Maybe (Discard m v)) alwaysValid = pure (const . const Nothing) cacheIO :: (MonadIO m, Hashable k) => CacheConfig m k v -> IO (Cache m k v) cacheIO = atomically . cache cache :: (MonadIO m, Hashable k) => CacheConfig m k v -> STM (Cache m k v) cache cacheConfig = mdo tail <- newTVar (Some head) entries <- SH.new finger <- newTVar tail head <- ListNode tail <$> newTVar tail <*> pure Head {..} pure $ Cache head cacheConfig cached :: Cache m k v -> k -> m v cached (Cache head@ListNode{prevNextPtrPtr=neck, elem=Head{..}} CacheConfig{..}) k = do checkValid <- validator tryMaybe -- Fast path: lookup value, update stats and return the value if found and valid ((liftIO . atomically) (lookup checkValid) >>= notify (requestListener . isJust) >>= validate) -- Slow path: load/calculate value and insert it (if still not found) (do value <- load k whileM (not <$> tryInsert value) pure value) where tryMaybe f notFound = f >>= maybe notFound pure notify = ((<$) <*>) validate = fmap join . traverse (\case -- valid value (Right v) -> pure $ Just v -- refresh value (Left (Refresh act)) -> act $> Nothing -- discard value and return alt result (Left (Invalid res)) -> Just <$> res) lookup checkValid = SH.focus focus (ekey . elem) k entries where focus = F.Focus -- not found (pure (Nothing, F.Leave)) -- found -- check entry validity (\e@ListNode{elem=Entry{visited, entryValue}} -> maybe -- entry valid (mark visited True $> (Just $ Right entryValue, F.Leave)) -- entry invalid -- remove it ((removeEntry e $>) . (, F.Remove) . Just . Left) (checkValid k entryValue) ) mark t b = whenM ((/= b) <$> readTVar t) (writeTVar t b) -- perform a single entry eviction and possibly insertion atomically -- returning False if could not insert -- (either because entry currently pointed by the finger was visited -- or because after this entry eviction the cache is still full) -- so that other threads don't have to wait when visiting entries. -- First check if entry is still not in the cache - this time inside transaction. -- -- Execute evictionListener if an entry was evicted tryInsert value = do (result, evicted) <- liftIO . atomically $ do -- Use SH.focus to performa a single lookup instead of 2 -- we cannot modify Hamt from inside focus -- so if there is any entry to remove -- we need to delete it after (res, evictedKey) <- SH.focus focus (ekey . elem) k entries case evictedKey of (Just Entry{ekey=entryKey, entryValue}) -> do SH.focus F.delete (ekey . elem) entryKey entries pure (res, evictionListener entryKey entryValue) Nothing -> pure (res, pure ()) evicted $> result where focus = F.Focus (do (hasSpace, evictedKey) <- evictionStep if hasSpace then do entry <- newLinkedEntry value -- done, maybe evicted, insert entry pure ((True, evictedKey), F.Set entry) else -- not done, maybe evicted, don't modify entries pure ((False, evictedKey), F.Leave)) -- Entry found case (\ListNode{elem=Entry{visited}} -> do -- mark as visited mark visited True -- done, no evictions, don't modify entries pure ((True, Nothing), F.Leave)) -- if the cache is full precoesses a single node -- removing it if it is marked as unvisited -- or clearing visited mark -- returns True if there is space in the cache -- puts evictionListener in state if an entry was evicted evictionStep = do currDiff <- liftA2 (-) (SH.size entries) (max 1 <$> maxSize) if currDiff >= 0 then do -- no space in the cache -- need to evict an entry (nextFinger, evictedKey) <- readTVar finger >>= evict writeTVar finger nextFinger -- return if enough space and evicted key if any pure (isJust evictedKey && currDiff == 0, evictedKey) else -- there is space in the cache pure (True, Nothing) evict :: TVar (Some (ListNode k v)) -> STM (NodePtr k v, Maybe (NodeElem k v True)) evict = readTVar >=> \case (Some e@ListNode{nextPtr, prevNextPtrPtr, elem=elem@Entry{visited}}) -> do ifM (readTVar visited) (writeTVar visited False $> (nextPtr, Nothing)) (unlinkEntry e *> fmap (, Just elem) (readTVar prevNextPtrPtr)) -- skip head (Some ListNode{nextPtr, elem=Head{}}) -> evict nextPtr unlinkEntry :: HamtEntry k v -> STM () unlinkEntry (ListNode{nextPtr, prevNextPtrPtr=currPrev}) = do nextEntry <- readTVar nextPtr withSome nextEntry $ \e -> do prevNextPtr <- readTVar currPrev writeTVar (prevNextPtrPtr e) prevNextPtr writeTVar prevNextPtr nextEntry newLinkedEntry v = do oldNeckNextPtr <- readTVar neck newNeckNextPtr <- newTVar (Some head) newNeck <- ListNode newNeckNextPtr <$> newTVar oldNeckNextPtr <*> (Entry <$> newTVar False <*> pure k <*> pure v) -- update pointers writeTVar oldNeckNextPtr (Some newNeck) writeTVar neck newNeckNextPtr -- return HAMT entry pure newNeck removeEntry = fmap (*>) unlinkEntry <*> adjustFinger adjustFinger ListNode{nextPtr, prevNextPtrPtr} = whenM ((nextPtr ==) <$> readTVar finger) $ readTVar prevNextPtrPtr >>= writeTVar finger