1
1
mirror of https://github.com/aelve/guide.git synced 2024-11-27 10:10:50 +03:00
guide/lib/Cache.hs

158 lines
5.5 KiB
Haskell
Raw Normal View History

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