11{-# LANGUAGE LambdaCase #-}
22{-# LANGUAGE NamedFieldPuns #-}
3+ {-# LANGUAGE QuasiQuotes #-}
34{-# LANGUAGE RecordWildCards #-}
45
56module PostgREST.AppState
@@ -35,7 +36,8 @@ import Data.Either.Combinators (whenLeft)
3536import qualified Data.Text as T (unpack )
3637import qualified Hasql.Pool as SQL
3738import qualified Hasql.Pool.Config as SQL
38- import qualified Hasql.Session as SQL
39+ import qualified Hasql.Session as SQL hiding (statement )
40+ import qualified Hasql.Transaction as SQL hiding (sql )
3941import qualified Hasql.Transaction.Sessions as SQL
4042import qualified Network.HTTP.Types.Status as HTTP
4143import qualified Network.Socket as NS
@@ -72,9 +74,16 @@ import PostgREST.SchemaCache (SchemaCache (..),
7274import PostgREST.SchemaCache.Identifiers (quoteQi )
7375import PostgREST.Unix (createAndBindDomainSocket )
7476
75- import Data.Streaming.Network (bindPortTCP , bindRandomPortTCP )
76- import Data.String (IsString (.. ))
77- import Protolude
77+ import Data.Functor.Contravariant ((>$<) )
78+ import Data.Streaming.Network (bindPortTCP ,
79+ bindRandomPortTCP )
80+ import Data.String (IsString (.. ))
81+ import qualified Hasql.Decoders as HD
82+ import qualified Hasql.Encoders as HE
83+ import qualified Hasql.Statement as SQL
84+ import NeatInterpolation (trimming )
85+ import Protolude
86+
7887
7988data AppState = AppState
8089 -- | Database connection pool
@@ -401,9 +410,15 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThrea
401410 qSchemaCache :: IO (Maybe SchemaCache )
402411 qSchemaCache = do
403412 conf@ AppConfig {.. } <- getConfig appState
413+ -- Allow 10 concurrent schema cache loads, guarded by advisory locks.
414+ -- This is to prevent thundering herd problem on startup or when many PostgREST
415+ -- instances receive "reload schema" notifications at the same time
416+ let withTxLock = SQL. statement (50168275 , 10 ) $
417+ SQL. Statement get_lock_sql get_lock_params HD. noResult configDbPreparedStatements
418+
404419 (resultTime, result) <-
405420 let transaction = if configDbPreparedStatements then SQL. transaction else SQL. unpreparedTransaction in
406- timeItT $ usePool appState (transaction SQL. ReadCommitted SQL. Read $ querySchemaCache conf)
421+ timeItT $ usePool appState (transaction SQL. ReadCommitted SQL. Read $ withTxLock *> querySchemaCache conf)
407422 case result of
408423 Left e -> do
409424 putSCacheStatus appState SCPending
@@ -421,6 +436,24 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThrea
421436 observer $ SchemaCacheLoadedObs t
422437 putSCacheStatus appState SCLoaded
423438 return $ Just sCache
439+ where
440+ -- recursive query that tries acquiring locks in order
441+ -- and waits for randomly selected lock if no attempt succeeded
442+ -- parameters are lock number and number of locks to try
443+ get_lock_sql = encodeUtf8 [trimming |
444+ WITH RECURSIVE attempts AS (
445+ SELECT 1 AS lock_number, pg_try_advisory_xact_lock($$1, 1) AS success WHERE $$2 > 0
446+ UNION ALL
447+ SELECT next_lock_number AS lock_number, pg_try_advisory_xact_lock($$1, next_lock_number) AS success FROM (
448+ SELECT lock_number + 1 AS next_lock_number FROM attempts
449+ WHERE NOT success AND lock_number < $$2
450+ ORDER BY lock_number DESC
451+ LIMIT 1
452+ ) AS previous_attempt
453+ )
454+ SELECT pg_advisory_xact_lock($$1, floor(random() * $$2)::int + 1) WHERE NOT EXISTS (SELECT 1 FROM attempts WHERE success) |]
455+
456+ get_lock_params = (fst >$< HE. param (HE. nonNullable HE. int4)) <> (snd >$< HE. param (HE. nonNullable HE. int4))
424457
425458 shouldRetry :: RetryStatus -> (Maybe PgVersion , Maybe SchemaCache ) -> IO Bool
426459 shouldRetry _ (pgVer, sCache) = do
0 commit comments