{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE FlexibleContexts #-} -- | Solution for caching mandatory data with Redis. -- -- In many cases, requires not just pick up or put the data into the cache. -- As a rule, data are required. -- -- ... check the cache ... if the value is missing, run the calculations ... -- put value to cache ... Tedious -- -- Solution is quite simple - collapse all of these steps in one operation. module Database.Redis.Pile ( pile ) where import Control.Monad.IO.Class (MonadIO) import qualified Data.ByteString as B import Data.Binary (Binary(..), encode, decode) import Data.String.Conversions ((<>), cs) import qualified Database.Redis as R import qualified Database.Redis.Tags as RT -- | Stores computation results in Redis. Computation fires only -- if data absent in cache. Of course, to refresh the data, they must first -- remove it from the cache. -- -- Computation controls everything except prefix and key. -- -- In background data is stored in Redis as HashSet with two fields: @d@ -- for serialized data and @e@ for expect field. -- -- Time complexity depends on the situation. -- -- * @O(2)@ data exists in cache, expect matches. -- -- * @O(2)@ data exists in cache, expect value is 'Nothing'. -- -- * @O(3)@ data exists in cache, but expect value not matches value -- in cache. -- -- * In all other cases time complexity does not make sense pile :: forall ma d t . (MonadIO ma, R.RedisCtx ma (Either t), Binary d) => B.ByteString -- ^ Prefix for key and tags. -> B.ByteString -- ^ Key in cache. Key will be stored as @prefix:key@ -> Maybe B.ByteString -- ^ Optional expect value. If it matches the value in the cache, -- 'pile' will return 'Nothing'. This is very useful when data in -- cache can be described with hash. For example, webpage ETag. -> (forall mb . (MonadIO mb) => mb (d, B.ByteString, [B.ByteString], Integer)) -- ^ Computation that returns data, expect value, tags and -- optional TTL (set it to zero for no expiration). -- All tags will be stored as @prefix:tag@. -> ma (Maybe d) pile keyPrefix key (Just ev) fn = do res <- R.hget (keyPrefix <> ":" <> key) "e" case res of Right (Just ev') | ev' == ev -> return Nothing | otherwise -> pile keyPrefix key Nothing fn _ -> pile keyPrefix key Nothing fn pile keyPrefix key Nothing fn = do res <- fetchPayload case res of Nothing -> runFn Just res' -> return . Just . decode . cs $ res' where withPrefix = keyPrefix <> ":" <> key fetchPayload = do v <- R.hget withPrefix "d" return $ case v of Right (Just v') -> Just v' _ -> Nothing runFn = do -- run and encode data (newData, newExpectValue, tags, ttl) <- fn let encodedData = cs . encode $ newData -- Try to get data. maybeInCache <- R.hget withPrefix "d" case maybeInCache of Right Nothing -> do -- no data in cache. store and return _ <- R.hmset withPrefix [("e", newExpectValue), ("d", encodedData)] setExpire ttl RT.markTags [withPrefix] keyPrefix tags return $ Just newData Right (Just cachedData) -> -- data in cache. dont set. just return return . Just . decode . cs $ cachedData _ -> -- some troubles. just return return $ Just newData where setExpire 0 = return () setExpire ke = do _ <- R.expire withPrefix ke return ()