freckle-app-1.0.3.0: library/Freckle/App/Memcached.hs
-- | App-level caching backed by Memcached
--
-- Usage:
--
-- 1. Have a Reader-like monad stack over some @App@
-- 2. Set up that @App@ with 'HasMemcachedClient'
-- 3. Give the value to cache a 'Cachable' instance
-- 4. Use 'caching'
--
-- To avoid 'Cachable', see 'cachingAs' and 'cachingAsJSON'.
--
module Freckle.App.Memcached
( Cachable(..)
, caching
, cachingAs
, cachingAsJSON
-- * Re-exports
, module Freckle.App.Memcached.Client
, module Freckle.App.Memcached.CacheKey
, module Freckle.App.Memcached.CacheTTL
) where
import Freckle.App.Prelude
import Control.Monad.Logger (MonadLogger, logErrorN)
import Data.Aeson
import Data.ByteString (ByteString)
import qualified Data.ByteString.Lazy as BSL
import Data.Text.Encoding (decodeUtf8With)
import Data.Text.Encoding.Error (lenientDecode)
import Freckle.App.Memcached.CacheKey
import Freckle.App.Memcached.CacheTTL
import Freckle.App.Memcached.Client (HasMemcachedClient(..))
import qualified Freckle.App.Memcached.Client as Memcached
import UnliftIO.Exception (Exception(..), handleAny)
class Cachable a where
toCachable :: a -> ByteString
fromCachable :: ByteString -> Either String a
instance Cachable ByteString where
toCachable = id
fromCachable = Right
instance Cachable BSL.ByteString where
toCachable = BSL.toStrict
fromCachable = Right . BSL.fromStrict
instance Cachable Text where
toCachable = encodeUtf8
fromCachable = Right . decodeUtf8With lenientDecode
data Cached a
= CacheFound a
| CacheNotFound
| CacheError Text
-- | Memoize an action using Memcached and 'Cachable'
caching
:: ( MonadUnliftIO m
, MonadLogger m
, MonadReader env m
, HasMemcachedClient env
, Cachable a
)
=> CacheKey
-> CacheTTL
-> m a
-> m a
caching = cachingAs fromCachable toCachable
-- | Like 'caching', but with explicit conversion functions
cachingAs
:: (MonadUnliftIO m, MonadLogger m, MonadReader env m, HasMemcachedClient env)
=> (ByteString -> Either String a)
-> (a -> ByteString)
-> CacheKey
-> CacheTTL
-> m a
-> m a
cachingAs from to key ttl f = do
result <-
fmap (maybe CacheNotFound (either (CacheError . pack) CacheFound . from))
$ handleCachingError Nothing "getting"
$ Memcached.get key
case result of
CacheFound a -> pure a
CacheNotFound -> store
CacheError e -> do
logCachingError "deserializing" e
store
where
store = do
a <- f
a <$ handleCachingError () "setting" (Memcached.set key (to a) ttl)
-- | Like 'caching', but de/serializing the value as JSON
cachingAsJSON
:: ( MonadUnliftIO m
, MonadLogger m
, MonadReader env m
, HasMemcachedClient env
, FromJSON a
, ToJSON a
)
=> CacheKey
-> CacheTTL
-> m a
-> m a
cachingAsJSON = cachingAs eitherDecodeStrict encodeStrict
handleCachingError
:: (MonadUnliftIO m, MonadLogger m) => a -> Text -> m a -> m a
handleCachingError value action = handleAny $ \ex -> do
logCachingError action $ pack $ displayException ex
pure value
logCachingError :: MonadLogger m => Text -> Text -> m ()
logCachingError action message =
logErrorN $ "[Caching] error " <> action <> ": " <> message
encodeStrict :: ToJSON a => a -> ByteString
encodeStrict = BSL.toStrict . encode