hedis-pile 0.4.2 → 0.5.0
raw patch · 3 files changed
+143/−120 lines, 3 filesdep +binarydep +string-conversionsdep ~transformersPVP ok
version bump matches the API change (PVP)
Dependencies added: binary, string-conversions
Dependency ranges changed: transformers
API changes (from Hackage documentation)
- Database.Redis.Pile: pile :: (MonadIO ma, ma ~ Redis) => ByteString -> ByteString -> Maybe (ByteString, ByteString) -> Maybe ByteString -> (forall mb. MonadIO mb => mb ([(ByteString, ByteString)], Maybe Integer, [ByteString])) -> ma (Maybe [(ByteString, ByteString)])
+ Database.Redis.Pile: pile :: (MonadIO ma, ma ~ Redis, Binary d, Show d) => ByteString -> ByteString -> Maybe ByteString -> (forall mb. MonadIO mb => mb (d, ByteString, [ByteString], Maybe Integer)) -> ma (Maybe d)
Files
- hedis-pile.cabal +7/−3
- src/Database/Redis/Pile.hs +61/−80
- test/Database/Redis/Test/Pile.hs +75/−37
hedis-pile.cabal view
@@ -1,5 +1,5 @@ name: hedis-pile -version: 0.4.2 +version: 0.5.0 cabal-version: >= 1.8 build-type: Simple stability: Experimental @@ -21,9 +21,11 @@ build-depends: base >= 4 && < 5, bytestring >= 0.9 && < 0.10, + binary, hedis >= 0.4 && < 0.5, hedis-tags >= 0.1, - transformers >= 0.2 && < 0.3 + string-conversions, + transformers >= 0.2 && < 0.4 ghc-options: -Wall exposed-modules: Database.Redis.Pile @@ -45,8 +47,10 @@ bytestring >= 0.9 && < 0.10, hedis >= 0.4 && < 0.5, + binary, lifted-base, - transformers >= 0.2 && < 0.3 + string-conversions, + transformers >= 0.2 && < 0.4 ghc-options: -Wall -rtsopts -threaded main-is: Main.hs other-modules: Database.Redis.Test.Pile
src/Database/Redis/Pile.hs view
@@ -1,4 +1,6 @@-{-# LANGUAGE OverloadedStrings, RankNTypes, TypeFamilies #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE TypeFamilies #-} -- | Solution for caching mandatory data with Redis. -- @@ -9,110 +11,89 @@ -- put value to cache ... Tedious -- -- Solution is quite simple - collapse all of these steps in one operation. + module Database.Redis.Pile ( + pile ) where -import qualified Data.ByteString as B - import Control.Monad.IO.Class (MonadIO) import Control.Monad (void) +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 -import Data.Maybe (fromJust) --- | Stores computation results in Redis as hashSet. Computation fires only +--import Control.Monad.IO.Class (liftIO) + +-- | Stores computation results in Redis. Computation fires only -- if data absent in cache. Of course, to refresh the data, they must first -- remove from the cache. -- --- Computation controls all that will be stored in the cache except two --- things: key and prefix for keys and tags. To do this, --- with the results of computation, it may return optional @TTL@ in --- seconds (Redis convention) and tags for key. About tags see --- "Database.Redis.Tags". --- --- Instead get all data from cache, optional parameter allows simply make --- sure that cache holds HashSet with needed field with needed value. If this --- is so, 'pile' return 'Nothing'. +-- Computation controls everything except prefix and key. -- --- > conn <- connect defaultConnectInfo --- > runRedis conn $ do --- > -- do it --- > r <- pile "myprefix" "mykey" (Just ("etag", "etag")) Nothing $ --- > return ([("etag", "etag"), ("val", "myval")], Nothing, []) --- > liftIO $ print r --- > -- Just [("etag", "etag"), ("val", "myval")] --- > --- > -- once again --- > r <- pile "myprefix" "mykey" (Just ("etag", "etag")) Nothing $ --- > return ([("etag", "etag"), ("val", "myval")], Nothing, []) --- > liftIO $ print r --- > -- Nothing --- > --- > -- and again without expect --- > r <- pile "myprefix" "mykey" Nothing Nothing $ --- > return ([("etag", "etag"), ("val", "myval")], Nothing, []) --- > liftIO $ print r --- > -- Just [("etag", "etag"), ("val", "myval")] +-- In background data is stored in Redis as HashSet with two fields: @d@ +-- for serialized data and @e@ for expect field. -- --- Time complexity for cached data: +-- Time complexity depends on the situation. -- --- * @O(1)@ If expect matches. --- --- * @O(2)@ If payload field provided -pile :: forall ma . (MonadIO ma, ma ~ R.Redis) => +-- * @O(1)@ if data exists in cache and expect matches. +-- +-- * @O(1)@ if data exists in cache and expect value is 'Nothing'. +-- +-- * @O(2)@ + +pile :: forall ma d . (MonadIO ma, ma ~ R.Redis, Binary d, Show d) => B.ByteString -- ^ Prefix for key and tags. -> B.ByteString -- ^ Key in cache. Key will be stored as @prefix:key@ - -> Maybe (B.ByteString, B.ByteString) - -- ^ Optional expect field. -> Maybe B.ByteString - -- ^ Optional payload field. This reduces time complexity of - -- request data from the cache from @O(N)@ to @O(1)@. - -- Regardless of setting this field, all data from computation - -- will stored in cache. - -> (forall mb . MonadIO mb => mb ([(B.ByteString, B.ByteString)], - Maybe Integer, - [B.ByteString])) - -- ^ Computation that returns data and - -- optional TTL and tags. All tags will be stored as @prefix:tag@. - -> ma (Maybe [(B.ByteString, B.ByteString)]) -pile p key (Just (ef, ev)) payload f = do - e <- R.hget (p `B.append` ":" `B.append` key) ef - case e of + -- ^ 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], Maybe Integer)) + -- ^ Computation that returns data, expect value, tags and + -- optional TTL. + -- All tags will be stored as @prefix:tag@. + -> ma (Maybe d) +pile prx key (Just ev) fn = do + res <- R.hget (prx <> ":" <> key) "e" + case res of Right (Just ev') | ev' == ev -> return Nothing - | otherwise -> pile p key Nothing payload f - _ -> pile p key Nothing payload f -pile p key Nothing payload f = do - d <- fetchPayload payload - case d of - Nothing -> runF - Just r -> return $ Just r + | otherwise -> pile prx key Nothing fn + _ -> pile prx key Nothing fn +pile prx key Nothing fn = do + res <- fetchPayload + case res of + Nothing -> runFn + Just res' -> return $ Just $ decode $ cs res' where - fetchPayload Nothing = do - v <- R.hgetall withPrefix - return $ case v of - Right [] -> Nothing - Right r -> Just r - _ -> Nothing - fetchPayload (Just plf) = do - v <- R.hget withPrefix plf + withPrefix = prx <> ":" <> key + fetchPayload = do + v <- R.hget withPrefix "d" return $ case v of - Right (Just v') -> Just [(plf, v')] + Right (Just v') -> Just v' _ -> Nothing - - withPrefix = p `B.append` ":" `B.append` key - runF = do - (r, ke, t) <- f - void $ R.hmset withPrefix r - setExpire ke - RT.markTags [withPrefix] p t - return $ case payload of - Nothing -> Just r - Just pl -> Just [(pl, fromJust $ lookup pl r)] + runFn = do + (dt, ev', ts, ttl) <- fn + let dt' = cs . encode $ dt + void $ R.hmset withPrefix [("e", ev'), ("d", dt')] + setExpire ttl + RT.markTags [withPrefix] prx ts + return $ Just dt where setExpire Nothing = return () setExpire (Just ke) = void $ R.expire withPrefix ke - + + + + + + + +
test/Database/Redis/Test/Pile.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE RankNTypes, ScopedTypeVariables #-} module Database.Redis.Test.Pile (tests) where @@ -12,65 +12,103 @@ import Control.Exception.Lifted (bracket_) import qualified Data.ByteString as B +import qualified Data.ByteString.Lazy as BL +import Data.String.Conversions ((<>), cs) + import qualified Database.Redis as R import qualified Database.Redis.Pile as RP +import Data.Binary (encode, decode) + tests :: Test tests = mutuallyExclusive $ testGroup "Pile" [ - testCase "New Data" caseNewData, - testCase "Stored data" caseStoredData + testCase "Binary test" caseBinary, + testCase "Just Put & Get" casePutGet, + testCase "Put and get without expect" caseWithoutTag, + testCase "Put and get with expect" caseWithTag ] -caseNewData :: Assertion -caseNewData = bracket_ +-- | Binary checks +caseBinary :: Assertion +caseBinary = do + let d1 = (1 :: Int, "a" :: B.ByteString) + let a1 = encode d1 + let d2 = decode a1 :: (Int, B.ByteString) + liftIO $ d1 @=? d2 + +-- | Just do put-get routine +casePutGet :: Assertion +casePutGet = bracket_ setup - teardown - $ runInRedis $ do - r <- RP.pile allPrefix "two" Nothing Nothing $ - return (allData, Nothing, []) - liftIO $ Just allData @=? r - void $ RP.pile allPrefix "three" Nothing Nothing $ - return (allData, Just 15, ["one"]) - ex <- R.ttl $ allPrefix `B.append` ":three" - liftIO $ Right 15 @=? ex - r' <- RP.pile allPrefix "three" Nothing (Just "anydata") $ - return (allData, Nothing, []) - liftIO $ Just [("anydata", "anydata")] @=? r' - r'' <- RP.pile allPrefix "three" Nothing (Just "anydata") $ - return (allData, Nothing, []) - liftIO $ Just [("anydata", "anydata")] @=? r'' + teardown $ runInRedis $ do + r <- RP.pile testPrefix (toBs 1) Nothing $ + return (testData 1, "exp", [], Nothing) + liftIO $ r @=? Just (testData 1) -caseStoredData :: Assertion -caseStoredData = bracket_ +-- | Work without tag +caseWithoutTag :: Assertion +caseWithoutTag = bracket_ setup - teardown - $ runInRedis $ do - r <- RP.pile allPrefix "one" Nothing Nothing $ - return (allData, Nothing, []) - liftIO $ Just allData @=? r - r' <- RP.pile allPrefix "one" (Just ("etag", "etag")) Nothing $ - return (allData, Nothing, []) - liftIO $ Nothing @=? r' + teardown $ runInRedis $ do + r1 <- RP.pile testPrefix (toBs 1) Nothing $ + return (testData 1, "exp", [], Nothing) + (r2 :: Maybe TData) <- RP.pile testPrefix (toBs 1) Nothing $ + return (testData 1, "exp", [], Nothing) + liftIO $ r1 @=? r2 +-- | Work with tag +caseWithTag :: Assertion +caseWithTag = bracket_ + setup + teardown $ runInRedis $ do + -- prepend data + _ <- RP.pile testPrefix (toBs 1) (Just "exp") $ + return (testData 1, "exp", [], Nothing) + -- retrieve with matching expect + r2 <- RP.pile testPrefix (toBs 1) (Just "exp") $ + return (testData 1, "exp", [], Nothing) + liftIO $ r2 @=? Nothing + -- retrieve with unmatching expect + r3 <- RP.pile testPrefix (toBs 1) (Just "exp_no_match") $ + return (testData 1, "exp", [], Nothing) + liftIO $ r3 @=? Just (testData 1) + +-- | Run in redis runInRedis :: forall b. R.Redis b -> IO b runInRedis a = do conn <- R.connect R.defaultConnectInfo R.runRedis conn a +-- | Test setup setup :: IO () setup = runInRedis $ - void $ R.hmset "piletest:one" allData + void $ R.hmset (testPrefix <> ":mark") [("mark", "mark")] --- | Purge all keys with 'allPrefix' +-- | Teardown. Purge all keys with 'allPrefix' teardown :: IO () teardown = runInRedis $ do - a <- R.keys $ allPrefix `B.append` "*" + a <- R.keys $ testPrefix <> "*" _ <- either undefined R.del a return () -allPrefix :: B.ByteString -allPrefix = "piletest" +-- | Common prefix +testPrefix :: B.ByteString +testPrefix = "piletest" -allData :: [(B.ByteString, B.ByteString)] -allData = [("etag", "etag"), ("anydata", "anydata")]+type TData = (Int, [(B.ByteString, Maybe B.ByteString)], BL.ByteString) + +-- | Common data +testData :: + Int -- ^ Param-param + -> TData +testData n = + (n, [(toBs n, Just $ toBs n)], toLBs n) + +-- | Convert int to bytestring +toBs :: Int -> B.ByteString +toBs n = cs . show $ n + +-- | Convert int to lazy bytestring +toLBs :: Int -> BL.ByteString +toLBs n = cs . show $ n