graphql-engine/server/src-lib/Hasura/Server/SchemaCacheRef.hs

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

134 lines
5.4 KiB
Haskell
Raw Normal View History

{-# LANGUAGE CPP #-}
module Hasura.Server.SchemaCacheRef
( SchemaCacheRef,
initialiseSchemaCacheRef,
withSchemaCacheUpdate,
readSchemaCacheRef,
getSchemaCache,
-- * Utility
logInconsistentMetadata,
)
where
import Control.Concurrent.MVar.Lifted
import Control.Concurrent.STM qualified as STM
import Control.Monad.Trans.Control (MonadBaseControl)
import Data.IORef
import Hasura.Logging qualified as L
import Hasura.Prelude hiding (get, put)
import Hasura.RQL.DDL.Schema
import Hasura.RQL.Types.Metadata.Object
import Hasura.RQL.Types.SchemaCache
import Hasura.Server.Logging
import Hasura.Server.Metrics
( ServerMetrics (smSchemaCacheMetadataResourceVersion),
)
import System.Metrics.Gauge (Gauge)
import System.Metrics.Gauge qualified as Gauge
-- | A mutable reference to a 'RebuildableSchemaCache', plus
--
-- * a write lock,
-- * update version tracking, and
-- * a gauge metric that tracks the metadata version of the 'SchemaCache'.
data SchemaCacheRef = SchemaCacheRef
{ -- | The idea behind explicit locking here is to
--
-- 1. Allow maximum throughput for serving requests (/v1/graphql) (as each
-- request reads the current schemacache)
-- 2. We don't want to process more than one request at any point of time
-- which would modify the schema cache as such queries are expensive.
--
-- Another option is to consider removing this lock in place of `_scrCache ::
-- MVar ...` if it's okay or in fact correct to block during schema update in
-- e.g. _wseGCtxMap. Vamshi says: It is theoretically possible to have a
-- situation (in between building new schemacache and before writing it to
-- the IORef) where we serve a request with a stale schemacache but I guess
-- it is an okay trade-off to pay for a higher throughput (I remember doing a
-- bunch of benchmarks to test this hypothesis).
_scrLock :: MVar (),
_scrCache :: IORef (RebuildableSchemaCache, SchemaCacheVer),
-- | The gauge metric that tracks the current metadata version.
--
-- Invariant: This gauge must be updated via 'updateMetadataVersionGauge'
-- whenever the _scrCache IORef is updated.
_scrMetadataVersionGauge :: Gauge
}
-- | Build a new 'SchemaCacheRef'
initialiseSchemaCacheRef ::
MonadIO m => ServerMetrics -> RebuildableSchemaCache -> m SchemaCacheRef
initialiseSchemaCacheRef serverMetrics schemaCache = liftIO $ do
cacheLock <- newMVar ()
cacheCell <- newIORef (schemaCache, initSchemaCacheVer)
let metadataVersionGauge = smSchemaCacheMetadataResourceVersion serverMetrics
updateMetadataVersionGauge metadataVersionGauge schemaCache
pure $ SchemaCacheRef cacheLock cacheCell metadataVersionGauge
-- | Set the 'SchemaCacheRef' to the 'RebuildableSchemaCache' produced by the
-- given action.
--
-- An internal lock ensures that at most one update to the 'SchemaCacheRef' may
-- proceed at a time.
withSchemaCacheUpdate ::
(MonadIO m, MonadBaseControl IO m) =>
SchemaCacheRef ->
L.Logger L.Hasura ->
Maybe (STM.TVar Bool) ->
m (a, RebuildableSchemaCache) ->
m a
withSchemaCacheUpdate (SchemaCacheRef lock cacheRef metadataVersionGauge) logger mLogCheckerTVar action =
withMVarMasked lock $ \() -> do
(!res, !newSC) <- action
liftIO $ do
-- update schemacache in IO reference
modifyIORef' cacheRef $ \(_, prevVer) ->
let !newVer = incSchemaCacheVer prevVer
in (newSC, newVer)
-- update metric with new metadata version
updateMetadataVersionGauge metadataVersionGauge newSC
let inconsistentObjectsList = scInconsistentObjs $ lastBuiltSchemaCache newSC
logInconsistentMetadata' = logInconsistentMetadata logger inconsistentObjectsList
-- log any inconsistent objects only once and not everytime this method is called
case mLogCheckerTVar of
Nothing -> do logInconsistentMetadata'
Just logCheckerTVar -> do
logCheck <- liftIO $ STM.readTVarIO logCheckerTVar
if null inconsistentObjectsList && logCheck
then do
STM.atomically $ STM.writeTVar logCheckerTVar False
else do
unless (logCheck || null inconsistentObjectsList) $ do
STM.atomically $ STM.writeTVar logCheckerTVar True
logInconsistentMetadata'
return res
-- | Read the contents of the 'SchemaCacheRef'
readSchemaCacheRef :: SchemaCacheRef -> IO (RebuildableSchemaCache, SchemaCacheVer)
readSchemaCacheRef scRef = readIORef $ _scrCache scRef
-- | Utility function. Read the latest 'SchemaCache' from the 'SchemaCacheRef'.
--
-- > getSchemaCache == fmap (lastBuiltSchemaCache . fst) . readSchemaCacheRef
getSchemaCache :: SchemaCacheRef -> IO SchemaCache
getSchemaCache scRef = lastBuiltSchemaCache . fst <$> readSchemaCacheRef scRef
-- | Utility function
logInconsistentMetadata :: L.Logger L.Hasura -> [InconsistentMetadata] -> IO ()
logInconsistentMetadata logger objs =
unless (null objs) $
L.unLogger logger $
mkInconsMetadataLog objs
-- Internal helper. Set the gauge metric to the metadata version of the schema
-- cache, if it exists.
updateMetadataVersionGauge :: MonadIO m => Gauge -> RebuildableSchemaCache -> m ()
updateMetadataVersionGauge metadataVersionGauge schemaCache = do
let metadataVersion = scMetadataResourceVersion . lastBuiltSchemaCache $ schemaCache
liftIO $ traverse_ (Gauge.set metadataVersionGauge . getMetadataResourceVersion) metadataVersion