2016-05-01 16:28:10 +03:00
|
|
|
|
{-# LANGUAGE
|
|
|
|
|
NoImplicitPrelude
|
|
|
|
|
#-}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
module Cache
|
|
|
|
|
(
|
|
|
|
|
CacheKey(..),
|
|
|
|
|
invalidateCache,
|
2016-07-21 16:26:08 +03:00
|
|
|
|
emptyCache,
|
2016-05-01 16:28:10 +03:00
|
|
|
|
cached,
|
|
|
|
|
)
|
|
|
|
|
where
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import BasePrelude hiding (Category)
|
|
|
|
|
-- Lenses
|
|
|
|
|
import Lens.Micro.Platform hiding ((&))
|
|
|
|
|
-- Monads and monad transformers
|
|
|
|
|
import Control.Monad.IO.Class
|
|
|
|
|
-- ByteString
|
|
|
|
|
import qualified Data.ByteString.Lazy as BSL
|
|
|
|
|
-- Concurrent map
|
|
|
|
|
import qualified STMContainers.Map as STMMap
|
2016-07-25 20:12:08 +03:00
|
|
|
|
import qualified Focus
|
2016-05-01 16:28:10 +03:00
|
|
|
|
import Data.Hashable
|
|
|
|
|
-- Lucid
|
|
|
|
|
import Lucid.Base
|
|
|
|
|
|
|
|
|
|
-- local
|
|
|
|
|
import Types
|
|
|
|
|
import Utils
|
|
|
|
|
|
|
|
|
|
|
2016-07-25 20:12:08 +03:00
|
|
|
|
-- Left = someone started rendering but haven't finished yet
|
|
|
|
|
-- Right = result of the render
|
|
|
|
|
type Cache = STMMap.Map CacheKey (Either Unique BSL.ByteString)
|
2016-05-01 16:28:10 +03:00
|
|
|
|
|
|
|
|
|
cache :: Cache
|
|
|
|
|
{-# NOINLINE cache #-}
|
|
|
|
|
cache = unsafePerformIO STMMap.newIO
|
|
|
|
|
|
|
|
|
|
data CacheKey
|
|
|
|
|
= CacheCategoryList
|
|
|
|
|
-- categories
|
2016-05-05 16:50:10 +03:00
|
|
|
|
| CacheCategory (Uid Category)
|
|
|
|
|
| CacheCategoryInfo (Uid Category)
|
|
|
|
|
| CacheCategoryNotes (Uid Category)
|
2016-05-01 16:28:10 +03:00
|
|
|
|
-- items
|
|
|
|
|
| CacheItem (Uid Item)
|
|
|
|
|
| CacheItemInfo (Uid Item)
|
|
|
|
|
| CacheItemDescription (Uid Item)
|
|
|
|
|
| CacheItemEcosystem (Uid Item)
|
|
|
|
|
| CacheItemTraits (Uid Item)
|
|
|
|
|
| CacheItemNotes (Uid Item)
|
|
|
|
|
deriving (Show, Eq, Generic)
|
|
|
|
|
|
|
|
|
|
instance Hashable CacheKey
|
|
|
|
|
|
|
|
|
|
cacheDepends :: GlobalState -> CacheKey -> [CacheKey]
|
|
|
|
|
cacheDepends gs key = case key of
|
2016-05-05 16:50:10 +03:00
|
|
|
|
CacheCategoryList -> [key]
|
|
|
|
|
CacheCategory _ -> [key, CacheCategoryList]
|
2016-05-22 14:43:46 +03:00
|
|
|
|
-- If the category's prosConsEnabled/ecosystemEnabled have changed, we'd
|
|
|
|
|
-- have to render *all* items differently (and CacheCategoryInfo includes
|
|
|
|
|
-- prosConsEnabled/ecosystemEnabled). See Note [enabled sections].
|
|
|
|
|
CacheCategoryInfo x ->
|
|
|
|
|
[key, CacheCategory x, CacheCategoryList] ++
|
|
|
|
|
-- A convoluted way to say “find category with uid x”
|
|
|
|
|
map CacheItem (gs ^.. categories.each.filtered(hasUid x).items.each.uid)
|
2016-05-05 16:50:10 +03:00
|
|
|
|
CacheCategoryNotes x -> [key, CacheCategory x, CacheCategoryList]
|
2016-05-01 16:28:10 +03:00
|
|
|
|
-- If the item's group has been changed, it can influence how other items
|
|
|
|
|
-- in the same category are rendered (specifically, their lists of groups
|
|
|
|
|
-- in iteminfo will change)
|
|
|
|
|
CacheItem x ->
|
|
|
|
|
let cat = findCategoryByItem x gs
|
|
|
|
|
in CacheCategory (cat^.uid) :
|
|
|
|
|
-- This will cover (CacheItem x) so we don't need to prepend it
|
|
|
|
|
-- manually like we did before with including 'key' into lists
|
|
|
|
|
map CacheItem (cat^..items.each.uid) ++
|
|
|
|
|
map CacheItemInfo (cat^..items.each.uid)
|
|
|
|
|
-- Be careful – if the presentation of the category list ever changes
|
|
|
|
|
-- (e.g. to include lists of items in categories, or their counts, or
|
|
|
|
|
-- something), we'd have to invalidate CacheCategoryList here too.
|
|
|
|
|
CacheItemInfo x ->
|
|
|
|
|
let cat = findCategoryByItem x gs
|
|
|
|
|
in CacheCategory (cat^.uid) :
|
|
|
|
|
map CacheItem (cat^..items.each.uid) ++
|
|
|
|
|
map CacheItemInfo (cat^..items.each.uid)
|
|
|
|
|
CacheItemDescription x ->
|
|
|
|
|
let cat = findCategoryByItem x gs
|
|
|
|
|
in [key, CacheItem x, CacheCategory (cat^.uid)]
|
|
|
|
|
CacheItemEcosystem x ->
|
|
|
|
|
let cat = findCategoryByItem x gs
|
|
|
|
|
in [key, CacheItem x, CacheCategory (cat^.uid)]
|
|
|
|
|
CacheItemTraits x ->
|
|
|
|
|
let cat = findCategoryByItem x gs
|
|
|
|
|
in [key, CacheItem x, CacheCategory (cat^.uid)]
|
|
|
|
|
CacheItemNotes x ->
|
|
|
|
|
let cat = findCategoryByItem x gs
|
|
|
|
|
in [key, CacheItem x, CacheCategory (cat^.uid)]
|
|
|
|
|
|
|
|
|
|
-- Note that cache invalidation has to happen before the actual action is
|
|
|
|
|
-- performed, because otherwise bad things can happen (e.g. if we're deleting
|
|
|
|
|
-- an item we have to invalidate the cache for its category, but if we've
|
|
|
|
|
-- already deleted the item we can't look up its category).
|
|
|
|
|
invalidateCache :: MonadIO m => GlobalState -> CacheKey -> m ()
|
|
|
|
|
invalidateCache gs key = liftIO $ atomically $ do
|
|
|
|
|
for_ (cacheDepends gs key) $ \k ->
|
|
|
|
|
STMMap.delete k cache
|
|
|
|
|
|
2016-07-21 16:26:08 +03:00
|
|
|
|
emptyCache :: MonadIO m => m ()
|
2016-07-25 17:55:10 +03:00
|
|
|
|
emptyCache = liftIO $ atomically $ STMMap.deleteAll cache
|
2016-07-21 16:26:08 +03:00
|
|
|
|
|
2016-07-25 19:47:49 +03:00
|
|
|
|
cached :: MonadIO m => CacheKey -> HtmlT IO () -> HtmlT m ()
|
2016-05-01 16:28:10 +03:00
|
|
|
|
cached key gen = do
|
2016-07-25 20:12:08 +03:00
|
|
|
|
-- If the item isn't in the cache, we'll have to render it, so we insert a
|
|
|
|
|
-- unique mark if the item wasn't found, and we do it in the same STM
|
|
|
|
|
-- transaction (so that nobody would be able to interfere)
|
|
|
|
|
uniq <- liftIO newUnique
|
|
|
|
|
mbRes <- liftSTM $ do
|
|
|
|
|
r <- STMMap.lookup key cache
|
|
|
|
|
when (isNothing r) $
|
|
|
|
|
STMMap.insert (Left uniq) key cache
|
|
|
|
|
return r
|
|
|
|
|
-- Okay, so. What's the result?
|
2016-05-01 16:28:10 +03:00
|
|
|
|
case mbRes of
|
2016-07-25 20:12:08 +03:00
|
|
|
|
-- The item was in the cache, so we just return it
|
|
|
|
|
Just (Right res) -> toHtmlRaw res
|
|
|
|
|
-- Someone else is already rendering this; we'll use their result when
|
|
|
|
|
-- it's ready
|
|
|
|
|
Just (Left _) -> do
|
|
|
|
|
liftIO (threadDelay 1000)
|
|
|
|
|
cached key gen
|
|
|
|
|
-- The item isn't in the cache, so we'll take on rendering it
|
|
|
|
|
Nothing -> do
|
|
|
|
|
-- If rendering doesn't succeed, we delete the mark so that someone
|
|
|
|
|
-- else would be able to render it
|
|
|
|
|
bs <- liftIO $ renderBST gen `onException`
|
|
|
|
|
liftSTM (deleteIfEq key (Left uniq) cache)
|
|
|
|
|
-- If rendering has succeeded, we write the rendered result
|
|
|
|
|
liftSTM (replaceIfEq key (Left uniq) (Right bs) cache)
|
2016-05-01 16:28:10 +03:00
|
|
|
|
toHtmlRaw bs
|
2016-07-25 20:12:08 +03:00
|
|
|
|
|
|
|
|
|
-- The *IfEq functions are used for extra safety (we'll never replace a newer
|
|
|
|
|
-- render with an older render accidentally)
|
|
|
|
|
|
|
|
|
|
deleteIfEq :: Eq v => STMMap.Key k => k -> v -> STMMap.Map k v -> STM ()
|
|
|
|
|
deleteIfEq k v m = STMMap.focus (Focus.updateM upd) k m
|
|
|
|
|
where upd a = return (if a == v then Nothing else Just a)
|
|
|
|
|
|
|
|
|
|
replaceIfEq :: Eq v => STMMap.Key k => k -> v -> v -> STMMap.Map k v -> STM ()
|
|
|
|
|
replaceIfEq k v v' m = STMMap.focus (Focus.adjustM upd) k m
|
|
|
|
|
where upd a = return (if a == v then v' else a)
|
|
|
|
|
|
|
|
|
|
liftSTM :: MonadIO m => STM a -> m a
|
|
|
|
|
liftSTM = liftIO . atomically
|