-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat: add metric pgrst_jwt_cache_size_bytes in admin server #3802
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,7 +4,7 @@ Description : PostgREST Jwt Authentication Result Cache. | |
|
||
This module provides functions to deal with the JWT cache | ||
-} | ||
{-# LANGUAGE NamedFieldPuns #-} | ||
{-# LANGUAGE CPP #-} | ||
module PostgREST.Auth.JwtCache | ||
( init | ||
, JwtCacheState | ||
|
@@ -18,30 +18,50 @@ import qualified Data.Scientific as Sci | |
|
||
import Data.Time.Clock (UTCTime, nominalDiffTimeToSeconds) | ||
import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds) | ||
import System.Clock (TimeSpec (..)) | ||
#ifdef JWT_CACHE_METRIC /* Include this in a non-profiled postgrest build */ | ||
import GHC.DataSize (recursiveSizeNF) | ||
#endif | ||
import System.Clock (TimeSpec (..)) | ||
|
||
import PostgREST.Auth.Types (AuthResult (..)) | ||
import PostgREST.Error (Error (..)) | ||
import PostgREST.Auth.Types (AuthResult (..)) | ||
import PostgREST.Error (Error (..)) | ||
import PostgREST.Observation (Observation (..), ObservationHandler) | ||
|
||
import Control.Debounce | ||
import Protolude | ||
|
||
newtype JwtCacheState = JwtCacheState | ||
{ jwtCache :: C.Cache ByteString AuthResult | ||
-- Jwt Cache State | ||
-- | ||
-- Calculating the size of each cache entry is an expensive operation. We don't | ||
-- want to recalculate the size of each entry after the cache eviction/purging. | ||
-- | ||
-- To avoid this, we store the size of each cache entry with the value of the | ||
-- cache entry as a tuple (AuthResult,SizeInBytes). Now after the purging | ||
-- operation, the size of cache entry will be evicted along with the entry and | ||
-- updating the cache size becomes a simple sum of all sizes that are store in | ||
-- the cache | ||
data JwtCacheState = JwtCacheState | ||
-- | Jwt Cache | ||
{ jwtCache :: C.Cache ByteString (AuthResult,SizeInBytes) | ||
-- | Calculate cache size with debounce | ||
, cacheSizeCalcDebounceTimeout :: MVar (IO ()) | ||
} | ||
|
||
type SizeInBytes = Int | ||
|
||
-- | Initialize JwtCacheState | ||
init :: IO JwtCacheState | ||
init = do | ||
cache <- C.newCache Nothing -- no default expiration | ||
return $ JwtCacheState cache | ||
JwtCacheState cache <$> newEmptyMVar | ||
|
||
-- | Used to retrieve and insert JWT to JWT Cache | ||
lookupJwtCache :: JwtCacheState -> ByteString -> Int -> IO (Either Error AuthResult) -> UTCTime -> IO (Either Error AuthResult) | ||
lookupJwtCache JwtCacheState{jwtCache} token maxLifetime parseJwt utc = do | ||
checkCache <- C.lookup jwtCache token | ||
authResult <- maybe parseJwt (pure . Right) checkCache | ||
lookupJwtCache :: JwtCacheState -> ByteString -> Int -> IO (Either Error AuthResult) -> UTCTime -> ObservationHandler -> IO (Either Error AuthResult) | ||
lookupJwtCache jwtCacheState token maxLifetime parseJwt utc observer = do | ||
checkCache <- C.lookup (jwtCache jwtCacheState) token | ||
authResult <- maybe parseJwt (pure . Right . fst) checkCache | ||
|
||
case (authResult,checkCache) of | ||
case (authResult, checkCache) of | ||
-- From comment: | ||
-- https://github.com/PostgREST/postgrest/pull/3801#discussion_r1857987914 | ||
-- | ||
|
@@ -56,13 +76,20 @@ lookupJwtCache JwtCacheState{jwtCache} token maxLifetime parseJwt utc = do | |
|
||
(Right res, Nothing) -> do -- cache miss | ||
|
||
-- get expiration time | ||
let timeSpec = getTimeSpec res maxLifetime utc | ||
|
||
-- purge expired cache entries | ||
C.purgeExpired jwtCache | ||
C.purgeExpired (jwtCache jwtCacheState) | ||
|
||
-- calculate size of the cache entry to store it with authResult | ||
sz <- calcCacheEntrySizeInBytes (token,res,timeSpec) | ||
|
||
-- insert new cache entry | ||
C.insert' jwtCache (Just timeSpec) token res | ||
-- insert new cache entry with byte size | ||
C.insert' (jwtCache jwtCacheState) (Just timeSpec) token (res,sz) | ||
|
||
-- calculate complete cache size with debounce and log it | ||
updateCacheSizeWithDebounce jwtCacheState observer | ||
steve-chavez marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
_ -> pure () | ||
|
||
|
@@ -77,3 +104,60 @@ getTimeSpec res maxLifetime utc = do | |
case expireJSON of | ||
Just (JSON.Number seconds) -> TimeSpec (sciToInt seconds - utcToSecs utc) 0 | ||
_ -> TimeSpec (fromIntegral maxLifetime :: Int64) 0 | ||
|
||
-- | Update JwtCacheSize Metric | ||
-- | ||
-- Runs the cache size calculation with debounce | ||
updateCacheSizeWithDebounce :: JwtCacheState -> ObservationHandler -> IO () | ||
updateCacheSizeWithDebounce jwtCacheState observer = do | ||
cSizeDebouncer <- tryReadMVar $ cacheSizeCalcDebounceTimeout jwtCacheState | ||
steve-chavez marked this conversation as resolved.
Show resolved
Hide resolved
|
||
case cSizeDebouncer of | ||
Just d -> d | ||
Nothing -> do | ||
newDebouncer <- | ||
mkDebounce defaultDebounceSettings | ||
-- debounceFreq is set to default 1 second | ||
{ debounceAction = calculateSizeThenLog | ||
, debounceEdge = leadingEdge -- logs at the start and the end | ||
} | ||
Comment on lines
+119
to
+122
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This sets it to 1 second. Maybe we need to increase it for better perf? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @taimoorzaeem How about adjusting Also you were able to run the benchmark locally before #3802 (comment), could you try here again? (it's going to take me longer to deploy AWS machines with postgrest-benchmark, we no longer really need to do this). |
||
putMVar (cacheSizeCalcDebounceTimeout jwtCacheState) newDebouncer | ||
newDebouncer | ||
where | ||
calculateSizeThenLog :: IO () | ||
calculateSizeThenLog = do | ||
entries <- C.toList $ jwtCache jwtCacheState | ||
-- extract the size from each entry and sum them all | ||
let size = sum [ sz | (_,(_,sz),_) <- entries] | ||
observer $ JwtCache size -- updates and logs the metric | ||
|
||
-- | Calculate JWT Cache Size in Bytes | ||
-- | ||
-- The cache size is updated by calculating the size of every | ||
-- cache entry and updating the metric. | ||
-- | ||
-- The cache entry consists of | ||
-- key :: ByteString | ||
-- value :: AuthReult | ||
-- expire value :: TimeSpec | ||
-- | ||
-- We calculate the size of each cache entry component | ||
-- by using recursiveSizeNF function which first evaluates | ||
-- the data structure to Normal Form and then calculate size. | ||
-- The normal form evaluation is necessary for accurate size | ||
-- calculation because haskell is lazy and we dont wanna count | ||
-- the size of large thunks (unevaluated expressions) | ||
calcCacheEntrySizeInBytes :: (ByteString, AuthResult, TimeSpec) -> IO Int | ||
#ifdef JWT_CACHE_METRIC /* Include this in a non-profiled postgrest build */ | ||
calcCacheEntrySizeInBytes entry = fromIntegral <$> getSize entry | ||
where | ||
getSize :: (ByteString, AuthResult, TimeSpec) -> IO Word | ||
getSize (bs, ar, ts) = do | ||
keySize <- recursiveSizeNF bs | ||
arClaimsSize <- recursiveSizeNF $ authClaims ar | ||
arRoleSize <- recursiveSizeNF $ authRole ar | ||
timeSpecSize <- liftA2 (+) (recursiveSizeNF (sec ts)) (recursiveSizeNF (nsec ts)) | ||
let sizeOfSizeEntryItself = 8 -- a constant 8 bytes size of each size entry in the cache | ||
return (keySize + arClaimsSize + arRoleSize + timeSpecSize + sizeOfSizeEntryItself) | ||
#else /* otherwise set it to 0 for a profiled build (used in memory-tests) */ | ||
calcCacheEntrySizeInBytes _ = return 0 | ||
#endif |
Uh oh!
There was an error while loading. Please reload this page.