-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathCaching.hs
More file actions
171 lines (155 loc) · 5.75 KB
/
Caching.hs
File metadata and controls
171 lines (155 loc) · 5.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module Share.Utils.Caching
( cachedResponse,
conditionallyCachedResponse,
causalIdCacheKey,
branchIdCacheKey,
toCached,
Cached,
ShouldCache (..),
)
where
import Data.Aeson (FromJSON, ToJSON (..))
import Data.Aeson qualified as Aeson
import Data.Aeson.Encoding qualified as Aeson
import Data.Binary.Builder qualified as Builder
import Data.ByteString qualified as BS
import Data.ByteString.Lazy.Char8 qualified as BL
import Data.Text.Encoding qualified as Text
import Database.Redis qualified as R
import Network.HTTP.Media
import Network.HTTP.Types qualified as HTTP
import Servant
import Share.Postgres.IDs (BranchHashId (..), CausalId (..))
import Share.Prelude
import Share.Web.App
import Share.Web.Authorization.Types qualified as AuthZ
data Cached ct a
= Cached BS.ByteString
instance MimeRender JSON (Cached JSON a) where
mimeRender _proxy = \case
Cached bs -> BL.fromStrict bs
instance (FromJSON a, ToJSON a) => ToJSON (Cached JSON a) where
toJSON (Cached bs) = toJSON $ Aeson.decode @a $ BL.fromStrict bs
toEncoding (Cached bs) = Aeson.unsafeToEncoding $ Builder.fromLazyByteString $ BL.fromStrict bs
-- | Wrap a response in caching.
-- This combinator knows whether a given access is privileged or not and will _not_ cache
-- private content.
cachedResponse ::
forall ct a.
(Servant.MimeRender ct a) =>
AuthZ.AuthZReceipt ->
-- | The name of the endpoint we're caching. Must be unique.
Text ->
-- | Cache Keys: All parameters which affect the response
[Text] ->
-- | How to generate the response if it's not in the cache.
WebApp a ->
WebApp (Cached ct a)
cachedResponse authzReceipt endpointName cacheParams action =
conditionallyCachedResponse authzReceipt endpointName cacheParams (Right <$> action)
<&> either absurd id
data ShouldCache = DoCache | DontCache
deriving (Eq)
toCached :: forall ct a. (Servant.MimeRender ct a) => a -> Cached ct a
toCached a = Cached . BL.toStrict $ Servant.mimeRender (Proxy @ct) a
-- | Like 'cachedResponse', but only cache (True, x) values.
conditionallyCachedResponse ::
forall ct e a.
(Servant.MimeRender ct a) =>
AuthZ.AuthZReceipt ->
-- | The name of the endpoint we're caching. Must be unique.
Text ->
-- | Cache Keys: All parameters which affect the response
[Text] ->
-- | How to generate the response if it's not in the cache. True means cache, false means don't cache.
WebApp (Either e a) ->
WebApp (Either e (Cached ct a))
conditionallyCachedResponse authzReceipt endpointName cacheParams action = do
requestIsCacheable <- shouldUseCaching
let mayCachingToken = AuthZ.getCacheability authzReceipt
let shouldUseCaching = requestIsCacheable && isJust mayCachingToken
mayCachedResponse <-
if shouldUseCaching
then getCachedResponse endpointName cacheParams
else pure Nothing
case mayCachedResponse of
Just cachedResponse -> pure $ Right cachedResponse
Nothing -> do
action >>= \case
Right a -> do
let cachedResponse :: Cached ct a
cachedResponse = toCached a
when (shouldUseCaching) do
-- Only actually cache the response if it's valid to do so.
whenJust mayCachingToken \ct ->
cacheResponse ct endpointName cacheParams cachedResponse
pure . Right $ cachedResponse
Left e -> pure $ Left e
-- | Cached responses expire if not accessed in 7 days.
-- Or, it could be evicted sooner if we run out of space.
responseCacheTTL :: Integer
responseCacheTTL =
(60 * 24 * 30) -- 30 days.
-- | Construct a cache key for a response.
cachedResponseKey ::
-- | The name of the current endpoint
Text ->
-- | All the parameters which might affect the response
[Text] ->
MediaType ->
BS.ByteString
cachedResponseKey endpointName cacheParams mediaType =
BS.intercalate ":" . fmap encode $ ("cached-response" : endpointName : "media-type" : tShow mediaType : cacheParams)
where
-- Escape all the ":" in the keys so malicious users can't manually create an invalid key.
encode :: Text -> ByteString
encode = HTTP.urlEncode False . Text.encodeUtf8
-- | Cache a response at the given key.
cacheResponse ::
forall ct a.
(Servant.Accept ct) =>
-- A proof that authorization was checked on this request and it was determined to be public and cacheable.
AuthZ.CachingToken ->
-- | The name of the current endpoint
Text ->
-- | All the parameters which might affect the response
[Text] ->
-- | The response to cache
Cached ct a ->
WebApp ()
cacheResponse _cachingToken endpointName params (Cached bs) = do
-- We ignore cache save errors, better to not hold up the response.
void $ R.setex key responseCacheTTL bs
where
key = cachedResponseKey endpointName params (Servant.contentType $ Proxy @ct)
-- | Get a cached response
getCachedResponse ::
forall ct a.
(Servant.Accept ct) =>
-- | The name of the current endpoint
Text ->
-- | All the parameters which might affect the response
[Text] ->
WebApp (Maybe (Cached ct a))
getCachedResponse endpointName params =
R.get key >>= \case
Right (Just bs) -> do
-- Refresh expiry
void $ R.expire key responseCacheTTL
pure . Just $ Cached bs
_ -> pure Nothing
where
key = cachedResponseKey endpointName params (Servant.contentType $ Proxy @ct)
causalIdCacheKey :: CausalId -> Text
causalIdCacheKey (CausalId causalIdInt) =
-- Causal Ids are globally unique and never re-used.
"causal-id:" <> tShow @Int32 causalIdInt
branchIdCacheKey :: BranchHashId -> Text
branchIdCacheKey (BranchHashId branchIdInt) =
-- Branch Ids are globally unique and never re-used.
"branch-id:" <> tShow @Int32 branchIdInt