Merge pull request #831 from yesodweb/cachedBy
add cachedBy, like cached but adds a key
This commit is contained in:
commit
bef44d962a
@ -145,6 +145,7 @@ module Yesod.Core.Handler
|
|||||||
, getMessageRender
|
, getMessageRender
|
||||||
-- * Per-request caching
|
-- * Per-request caching
|
||||||
, cached
|
, cached
|
||||||
|
, cachedBy
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Data.Time (UTCTime, addUTCTime,
|
import Data.Time (UTCTime, addUTCTime,
|
||||||
@ -187,10 +188,9 @@ import Yesod.Core.Content (ToTypedContent (..), simpleConte
|
|||||||
import Yesod.Core.Internal.Util (formatRFC1123)
|
import Yesod.Core.Internal.Util (formatRFC1123)
|
||||||
import Text.Blaze.Html (preEscapedToMarkup, toHtml)
|
import Text.Blaze.Html (preEscapedToMarkup, toHtml)
|
||||||
|
|
||||||
import Data.Dynamic (fromDynamic, toDyn)
|
|
||||||
import qualified Data.IORef.Lifted as I
|
import qualified Data.IORef.Lifted as I
|
||||||
import Data.Maybe (listToMaybe, mapMaybe)
|
import Data.Maybe (listToMaybe, mapMaybe)
|
||||||
import Data.Typeable (Typeable, typeOf)
|
import Data.Typeable (Typeable)
|
||||||
import Web.PathPieces (PathPiece(..))
|
import Web.PathPieces (PathPiece(..))
|
||||||
import Yesod.Core.Class.Handler
|
import Yesod.Core.Class.Handler
|
||||||
import Yesod.Core.Types
|
import Yesod.Core.Types
|
||||||
@ -208,6 +208,7 @@ import Control.Monad.Trans.Control (control, MonadBaseControl)
|
|||||||
import Data.Conduit (Source, transPipe, Flush (Flush), yield, Producer
|
import Data.Conduit (Source, transPipe, Flush (Flush), yield, Producer
|
||||||
, Sink
|
, Sink
|
||||||
)
|
)
|
||||||
|
import qualified Yesod.Core.TypeCache as Cache
|
||||||
|
|
||||||
get :: MonadHandler m => m GHState
|
get :: MonadHandler m => m GHState
|
||||||
get = liftHandlerT $ HandlerT $ I.readIORef . handlerState
|
get = liftHandlerT $ HandlerT $ I.readIORef . handlerState
|
||||||
@ -351,6 +352,7 @@ handlerToIO =
|
|||||||
return $ oldState { ghsRBC = Nothing
|
return $ oldState { ghsRBC = Nothing
|
||||||
, ghsIdent = 1
|
, ghsIdent = 1
|
||||||
, ghsCache = mempty
|
, ghsCache = mempty
|
||||||
|
, ghsCacheBy = mempty
|
||||||
, ghsHeaders = mempty }
|
, ghsHeaders = mempty }
|
||||||
|
|
||||||
-- xx From this point onwards, no references to oldHandlerData xx
|
-- xx From this point onwards, no references to oldHandlerData xx
|
||||||
@ -851,34 +853,47 @@ getMessageRender = do
|
|||||||
l <- reqLangs `liftM` getRequest
|
l <- reqLangs `liftM` getRequest
|
||||||
return $ renderMessage (rheSite env) l
|
return $ renderMessage (rheSite env) l
|
||||||
|
|
||||||
-- | Use a per-request cache to avoid performing the same action multiple
|
-- | Use a per-request cache to avoid performing the same action multiple times.
|
||||||
-- times. Note that values are stored by their type. Therefore, you should use
|
-- Values are stored by their type, the result of typeOf from Typeable.
|
||||||
-- newtype wrappers to distinguish logically different types.
|
-- Therefore, you should use different newtype wrappers at each cache site.
|
||||||
|
--
|
||||||
|
-- For example, yesod-auth uses an un-exported newtype, CachedMaybeAuth and exports functions that utilize it such as maybeAuth.
|
||||||
|
-- This means that another module can create its own newtype wrapper to cache the same type from a different action without any cache conflicts.
|
||||||
|
--
|
||||||
|
-- See the original announcement: <http://www.yesodweb.com/blog/2013/03/yesod-1-2-cleaner-internals>
|
||||||
--
|
--
|
||||||
-- Since 1.2.0
|
-- Since 1.2.0
|
||||||
cached :: (MonadHandler m, Typeable a)
|
cached :: (MonadHandler m, Typeable a)
|
||||||
=> m a
|
=> m a
|
||||||
-> m a
|
-> m a
|
||||||
cached f = do
|
cached action = do
|
||||||
gs <- get
|
gs <- get
|
||||||
let cache = ghsCache gs
|
eres <- Cache.cached (ghsCache gs) action
|
||||||
case clookup cache of
|
case eres of
|
||||||
Just val -> return val
|
Right res -> return res
|
||||||
Nothing -> do
|
Left (newCache, res) -> do
|
||||||
val <- f
|
put $ gs { ghsCache = newCache }
|
||||||
put $ gs { ghsCache = cinsert val cache }
|
return res
|
||||||
return val
|
|
||||||
where
|
|
||||||
clookup :: Typeable a => Cache -> Maybe a
|
|
||||||
clookup (Cache m) =
|
|
||||||
res
|
|
||||||
where
|
|
||||||
res = Map.lookup (typeOf $ fromJust res) m >>= fromDynamic
|
|
||||||
fromJust :: Maybe a -> a
|
|
||||||
fromJust = error "Yesod.Handler.cached.fromJust: Argument to typeOf was evaluated"
|
|
||||||
|
|
||||||
cinsert :: Typeable a => a -> Cache -> Cache
|
-- | a per-request cache. just like 'cached'.
|
||||||
cinsert v (Cache m) = Cache (Map.insert (typeOf v) (toDyn v) m)
|
-- 'cached' can only cache a single value per type.
|
||||||
|
-- 'cachedBy' stores multiple values per type by usage of a ByteString key
|
||||||
|
--
|
||||||
|
-- 'cached' is ideal to cache an action that has only one value of a type, such as the session's current user
|
||||||
|
-- 'cachedBy' is required if the action has parameters and can return multiple values per type.
|
||||||
|
-- You can turn those parameters into a ByteString cache key.
|
||||||
|
-- For example, caching a lookup of a Link by a token where multiple token lookups might be performed.
|
||||||
|
--
|
||||||
|
-- Since 1.4.0
|
||||||
|
cachedBy :: (MonadHandler m, Typeable a) => S.ByteString -> m a -> m a
|
||||||
|
cachedBy k action = do
|
||||||
|
gs <- get
|
||||||
|
eres <- Cache.cachedBy (ghsCacheBy gs) k action
|
||||||
|
case eres of
|
||||||
|
Right res -> return res
|
||||||
|
Left (newCache, res) -> do
|
||||||
|
put $ gs { ghsCacheBy = newCache }
|
||||||
|
return res
|
||||||
|
|
||||||
-- | Get the list of supported languages supplied by the user.
|
-- | Get the list of supported languages supplied by the user.
|
||||||
--
|
--
|
||||||
|
|||||||
@ -74,6 +74,7 @@ runHandler rhe@RunHandlerEnv {..} handler yreq = withInternalState $ \resState -
|
|||||||
, ghsRBC = Nothing
|
, ghsRBC = Nothing
|
||||||
, ghsIdent = 1
|
, ghsIdent = 1
|
||||||
, ghsCache = mempty
|
, ghsCache = mempty
|
||||||
|
, ghsCacheBy = mempty
|
||||||
, ghsHeaders = mempty
|
, ghsHeaders = mempty
|
||||||
}
|
}
|
||||||
let hd = HandlerData
|
let hd = HandlerData
|
||||||
|
|||||||
82
yesod-core/Yesod/Core/TypeCache.hs
Normal file
82
yesod-core/Yesod/Core/TypeCache.hs
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
-- | a module for caching a monadic action based on its return type
|
||||||
|
--
|
||||||
|
-- The cache is a HashMap where the key uses the result of typeOf from Typeable.
|
||||||
|
-- The value stored is toDyn from Dynamic to support arbitrary value types.
|
||||||
|
--
|
||||||
|
-- un-exported newtype wrappers should be used to maintain unique keys in the cache.
|
||||||
|
--
|
||||||
|
-- used in 'Yesod.Core.Handler.cached' and 'Yesod.Core.Handler.cachedBy'
|
||||||
|
module Yesod.Core.TypeCache where
|
||||||
|
|
||||||
|
import Prelude hiding (lookup)
|
||||||
|
import Data.Typeable (Typeable, TypeRep, typeOf)
|
||||||
|
import Data.HashMap.Strict
|
||||||
|
import Data.ByteString (ByteString)
|
||||||
|
import Data.Dynamic (Dynamic, toDyn, fromDynamic)
|
||||||
|
|
||||||
|
type TypeMap = HashMap TypeRep Dynamic
|
||||||
|
type KeyedTypeMap = HashMap (TypeRep, ByteString) Dynamic
|
||||||
|
|
||||||
|
-- | avoid performing the same action multiple times.
|
||||||
|
-- Values are stored by their type, the result of typeOf from Typeable.
|
||||||
|
-- Therefore, you should use different newtype wrappers at each cache site.
|
||||||
|
--
|
||||||
|
-- For example, yesod-auth uses an un-exported newtype, CachedMaybeAuth and exports functions that utilize it such as maybeAuth.
|
||||||
|
-- This means that another module can create its own newtype wrapper to cache the same type from a different action without any cache conflicts.
|
||||||
|
--
|
||||||
|
-- In Yesod, this is used for a cache that is cleared at the end of every request.
|
||||||
|
-- See the original announcement: <http://www.yesodweb.com/blog/2013/03/yesod-1-2-cleaner-internals>
|
||||||
|
--
|
||||||
|
-- Since 1.2.0
|
||||||
|
cached :: (Monad m, Typeable a)
|
||||||
|
=> TypeMap
|
||||||
|
-> m a
|
||||||
|
-> m (Either (TypeMap, a) a) -- ^ Left is a cache miss, Right is a hit
|
||||||
|
cached cache action = case clookup cache of
|
||||||
|
Just val -> return $ Right val
|
||||||
|
Nothing -> do
|
||||||
|
val <- action
|
||||||
|
return $ Left (cinsert val cache, val)
|
||||||
|
where
|
||||||
|
clookup :: Typeable a => TypeMap -> Maybe a
|
||||||
|
clookup c =
|
||||||
|
res
|
||||||
|
where
|
||||||
|
res = lookup (typeOf $ fromJust res) c >>= fromDynamic
|
||||||
|
fromJust :: Maybe a -> a
|
||||||
|
fromJust = error "Yesod.Handler.cached.fromJust: Argument to typeOf was evaluated"
|
||||||
|
|
||||||
|
cinsert :: Typeable a => a -> TypeMap -> TypeMap
|
||||||
|
cinsert v = insert (typeOf v) (toDyn v)
|
||||||
|
|
||||||
|
-- | like 'cached'.
|
||||||
|
-- 'cached' can only cache a single value per type.
|
||||||
|
-- 'cachedBy' stores multiple values per type by usage of a ByteString key
|
||||||
|
--
|
||||||
|
-- 'cached' is ideal to cache an action that has only one value of a type, such as the session's current user
|
||||||
|
-- 'cachedBy' is required if the action has parameters and can return multiple values per type.
|
||||||
|
-- You can turn those parameters into a ByteString cache key.
|
||||||
|
-- For example, caching a lookup of a Link by a token where multiple token lookups might be performed.
|
||||||
|
--
|
||||||
|
-- Since 1.4.0
|
||||||
|
cachedBy :: (Monad m, Typeable a)
|
||||||
|
=> KeyedTypeMap
|
||||||
|
-> ByteString
|
||||||
|
-> m a
|
||||||
|
-> m (Either (KeyedTypeMap, a) a) -- ^ Left is a cache miss, Right is a hit
|
||||||
|
cachedBy cache k action = case clookup k cache of
|
||||||
|
Just val -> return $ Right val
|
||||||
|
Nothing -> do
|
||||||
|
val <- action
|
||||||
|
return $ Left (cinsert k val cache, val)
|
||||||
|
where
|
||||||
|
clookup :: Typeable a => ByteString -> KeyedTypeMap -> Maybe a
|
||||||
|
clookup key c =
|
||||||
|
res
|
||||||
|
where
|
||||||
|
res = lookup (typeOf $ fromJust res, key) c >>= fromDynamic
|
||||||
|
fromJust :: Maybe a -> a
|
||||||
|
fromJust = error "Yesod.Handler.cached.fromJust: Argument to typeOf was evaluated"
|
||||||
|
|
||||||
|
cinsert :: Typeable a => ByteString -> a -> KeyedTypeMap -> KeyedTypeMap
|
||||||
|
cinsert key v = insert (typeOf v, key) (toDyn v)
|
||||||
@ -25,7 +25,6 @@ import Control.Monad.Trans.Resource (MonadResource (..), Interna
|
|||||||
import Data.ByteString (ByteString)
|
import Data.ByteString (ByteString)
|
||||||
import qualified Data.ByteString.Lazy as L
|
import qualified Data.ByteString.Lazy as L
|
||||||
import Data.Conduit (Flush, Source)
|
import Data.Conduit (Flush, Source)
|
||||||
import Data.Dynamic (Dynamic)
|
|
||||||
import Data.IORef (IORef)
|
import Data.IORef (IORef)
|
||||||
import Data.Map (Map, unionWith)
|
import Data.Map (Map, unionWith)
|
||||||
import qualified Data.Map as Map
|
import qualified Data.Map as Map
|
||||||
@ -39,7 +38,6 @@ import qualified Data.Text as T
|
|||||||
import qualified Data.Text.Lazy.Builder as TBuilder
|
import qualified Data.Text.Lazy.Builder as TBuilder
|
||||||
import Data.Time (UTCTime)
|
import Data.Time (UTCTime)
|
||||||
import Data.Typeable (Typeable)
|
import Data.Typeable (Typeable)
|
||||||
import Data.Typeable (TypeRep)
|
|
||||||
import Language.Haskell.TH.Syntax (Loc)
|
import Language.Haskell.TH.Syntax (Loc)
|
||||||
import qualified Network.HTTP.Types as H
|
import qualified Network.HTTP.Types as H
|
||||||
import Network.Wai (FilePart,
|
import Network.Wai (FilePart,
|
||||||
@ -59,6 +57,7 @@ import Control.Monad.Reader (MonadReader (..))
|
|||||||
import Prelude hiding (catch)
|
import Prelude hiding (catch)
|
||||||
import Control.DeepSeq (NFData (rnf))
|
import Control.DeepSeq (NFData (rnf))
|
||||||
import Data.Conduit.Lazy (MonadActive, monadActive)
|
import Data.Conduit.Lazy (MonadActive, monadActive)
|
||||||
|
import Yesod.Core.TypeCache (TypeMap, KeyedTypeMap)
|
||||||
|
|
||||||
-- Sessions
|
-- Sessions
|
||||||
type SessionMap = Map Text ByteString
|
type SessionMap = Map Text ByteString
|
||||||
@ -162,9 +161,6 @@ type BottomOfHeadAsync master
|
|||||||
-> Maybe (HtmlUrl (Route master)) -- ^ widget of js to run on async completion
|
-> Maybe (HtmlUrl (Route master)) -- ^ widget of js to run on async completion
|
||||||
-> (HtmlUrl (Route master)) -- ^ widget to insert at the bottom of <head>
|
-> (HtmlUrl (Route master)) -- ^ widget to insert at the bottom of <head>
|
||||||
|
|
||||||
newtype Cache = Cache (Map TypeRep Dynamic)
|
|
||||||
deriving Monoid
|
|
||||||
|
|
||||||
type Texts = [Text]
|
type Texts = [Text]
|
||||||
|
|
||||||
-- | Wrap up a normal WAI application as a Yesod subsite.
|
-- | Wrap up a normal WAI application as a Yesod subsite.
|
||||||
@ -223,7 +219,8 @@ data GHState = GHState
|
|||||||
{ ghsSession :: SessionMap
|
{ ghsSession :: SessionMap
|
||||||
, ghsRBC :: Maybe RequestBodyContents
|
, ghsRBC :: Maybe RequestBodyContents
|
||||||
, ghsIdent :: Int
|
, ghsIdent :: Int
|
||||||
, ghsCache :: Cache
|
, ghsCache :: TypeMap
|
||||||
|
, ghsCacheBy :: KeyedTypeMap
|
||||||
, ghsHeaders :: Endo [Header]
|
, ghsHeaders :: Endo [Header]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,12 +6,15 @@ module YesodCoreTest.Cache (cacheTest, Widget) where
|
|||||||
|
|
||||||
import Test.Hspec
|
import Test.Hspec
|
||||||
|
|
||||||
|
import Network.Wai
|
||||||
import Network.Wai.Test
|
import Network.Wai.Test
|
||||||
|
|
||||||
import Yesod.Core
|
import Yesod.Core
|
||||||
import Data.IORef.Lifted
|
import Data.IORef.Lifted
|
||||||
import Data.Typeable (Typeable)
|
import Data.Typeable (Typeable)
|
||||||
import qualified Data.ByteString.Lazy.Char8 as L8
|
import qualified Data.ByteString.Lazy.Char8 as L8
|
||||||
|
import Data.Text (Text)
|
||||||
|
import Data.Text.Encoding (encodeUtf8)
|
||||||
|
|
||||||
data C = C
|
data C = C
|
||||||
|
|
||||||
@ -21,9 +24,13 @@ newtype V1 = V1 Int
|
|||||||
newtype V2 = V2 Int
|
newtype V2 = V2 Int
|
||||||
deriving Typeable
|
deriving Typeable
|
||||||
|
|
||||||
mkYesod "C" [parseRoutes|/ RootR GET|]
|
mkYesod "C" [parseRoutes|
|
||||||
|
/ RootR GET
|
||||||
|
/key KeyR GET
|
||||||
|
|]
|
||||||
|
|
||||||
instance Yesod C
|
instance Yesod C where
|
||||||
|
errorHandler e = liftIO (print e) >> defaultErrorHandler e
|
||||||
|
|
||||||
getRootR :: Handler RepPlain
|
getRootR :: Handler RepPlain
|
||||||
getRootR = do
|
getRootR = do
|
||||||
@ -36,16 +43,32 @@ getRootR = do
|
|||||||
|
|
||||||
return $ RepPlain $ toContent $ show [v1a, v1b, v2a, v2b]
|
return $ RepPlain $ toContent $ show [v1a, v1b, v2a, v2b]
|
||||||
|
|
||||||
|
getKeyR :: Handler RepPlain
|
||||||
|
getKeyR = do
|
||||||
|
ref <- newIORef 0
|
||||||
|
V1 v1a <- cachedBy "1" $ atomicModifyIORef ref $ \i -> (i + 1, V1 $ i + 1)
|
||||||
|
V1 v1b <- cachedBy "1" $ atomicModifyIORef ref $ \i -> (i + 1, V1 $ i + 1)
|
||||||
|
|
||||||
|
V2 v2a <- cachedBy "1" $ atomicModifyIORef ref $ \i -> (i + 1, V2 $ i + 1)
|
||||||
|
V2 v2b <- cachedBy "1" $ atomicModifyIORef ref $ \i -> (i + 1, V2 $ i + 1)
|
||||||
|
|
||||||
|
V2 v3a <- cachedBy "2" $ atomicModifyIORef ref $ \i -> (i + 1, V2 $ i + 1)
|
||||||
|
V2 v3b <- cachedBy "2" $ atomicModifyIORef ref $ \i -> (i + 1, V2 $ i + 1)
|
||||||
|
|
||||||
|
return $ RepPlain $ toContent $ show [v1a, v1b, v2a, v2b, v3a, v3b]
|
||||||
|
|
||||||
cacheTest :: Spec
|
cacheTest :: Spec
|
||||||
cacheTest =
|
cacheTest =
|
||||||
describe "Test.Cache" $ do
|
describe "Test.Cache" $ do
|
||||||
it "works" works
|
it "cached" $ runner $ do
|
||||||
|
res <- request defaultRequest
|
||||||
|
assertStatus 200 res
|
||||||
|
assertBody (L8.pack $ show [1, 1, 2, 2 :: Int]) res
|
||||||
|
|
||||||
|
it "cachedBy" $ runner $ do
|
||||||
|
res <- request defaultRequest { pathInfo = ["key"] }
|
||||||
|
assertStatus 200 res
|
||||||
|
assertBody (L8.pack $ show [1, 1, 2, 2, 3, 3 :: Int]) res
|
||||||
|
|
||||||
runner :: Session () -> IO ()
|
runner :: Session () -> IO ()
|
||||||
runner f = toWaiApp C >>= runSession f
|
runner f = toWaiApp C >>= runSession f
|
||||||
|
|
||||||
works :: IO ()
|
|
||||||
works = runner $ do
|
|
||||||
res <- request defaultRequest
|
|
||||||
assertStatus 200 res
|
|
||||||
assertBody (L8.pack $ show [1, 1, 2, 2 :: Int]) res
|
|
||||||
|
|||||||
@ -40,6 +40,7 @@ library
|
|||||||
, cereal >= 0.3
|
, cereal >= 0.3
|
||||||
, old-locale >= 1.0.0.2 && < 1.1
|
, old-locale >= 1.0.0.2 && < 1.1
|
||||||
, containers >= 0.2
|
, containers >= 0.2
|
||||||
|
, unordered-containers >= 0.2
|
||||||
, monad-control >= 0.3 && < 0.4
|
, monad-control >= 0.3 && < 0.4
|
||||||
, transformers-base >= 0.4
|
, transformers-base >= 0.4
|
||||||
, cookie >= 0.4.1 && < 0.5
|
, cookie >= 0.4.1 && < 0.5
|
||||||
@ -84,6 +85,7 @@ library
|
|||||||
Yesod.Core.Class.Yesod
|
Yesod.Core.Class.Yesod
|
||||||
Yesod.Core.Class.Dispatch
|
Yesod.Core.Class.Dispatch
|
||||||
Yesod.Core.Class.Breadcrumbs
|
Yesod.Core.Class.Breadcrumbs
|
||||||
|
Yesod.Core.TypeCache
|
||||||
Paths_yesod_core
|
Paths_yesod_core
|
||||||
|
|
||||||
Yesod.Routes.TH
|
Yesod.Routes.TH
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user