1- {-# LANGUAGE LambdaCase #-}
2- {-# LANGUAGE NamedFieldPuns #-}
3- {-# LANGUAGE RecordWildCards #-}
1+ {-# LANGUAGE LambdaCase #-}
2+ {-# LANGUAGE NamedFieldPuns #-}
3+ {-# LANGUAGE QuasiQuotes #-}
4+ {-# LANGUAGE RecordWildCards #-}
5+ {-# LANGUAGE TypeApplications #-}
46
57module PostgREST.AppState
68 ( AppState
@@ -36,7 +38,8 @@ import Data.Either.Combinators (whenLeft)
3638import qualified Data.Text as T (unpack )
3739import qualified Hasql.Pool as SQL
3840import qualified Hasql.Pool.Config as SQL
39- import qualified Hasql.Session as SQL
41+ import qualified Hasql.Session as SQL hiding (statement )
42+ import qualified Hasql.Transaction as SQL hiding (sql )
4043import qualified Hasql.Transaction.Sessions as SQL
4144import qualified Network.HTTP.Types.Status as HTTP
4245import qualified Network.Socket as NS
@@ -73,9 +76,16 @@ import PostgREST.SchemaCache (SchemaCache (..),
7376import PostgREST.SchemaCache.Identifiers (quoteQi )
7477import PostgREST.Unix (createAndBindDomainSocket )
7578
76- import Data.Streaming.Network (bindPortTCP , bindRandomPortTCP )
77- import Data.String (IsString (.. ))
78- import Protolude
79+ import Data.Streaming.Network (bindPortTCP ,
80+ bindRandomPortTCP )
81+ import Data.String (IsString (.. ))
82+ import qualified Hasql.Decoders as HD
83+ import qualified Hasql.Encoders as HE
84+ import qualified Hasql.Statement as SQL
85+ import NeatInterpolation (trimming )
86+ import PostgREST.Metrics (MetricsState (connTrack ))
87+ import Protolude
88+
7989
8090data AppState = AppState
8191 -- | Database connection pool
@@ -360,7 +370,7 @@ getObserver = stateObserver
360370-- + Because connections cache the pg catalog(see #2620)
361371-- + For rapid recovery. Otherwise, the pool idle or lifetime timeout would have to be reached for new healthy connections to be acquired.
362372retryingSchemaCacheLoad :: AppState -> IO ()
363- retryingSchemaCacheLoad appState@ AppState {stateObserver= observer, stateMainThreadId= mainThreadId} =
373+ retryingSchemaCacheLoad appState@ AppState {stateObserver= observer, stateMainThreadId= mainThreadId, stateMetrics } =
364374 void $ retrying retryPolicy shouldRetry (\ RetryStatus {rsIterNumber, rsPreviousDelay} -> do
365375 when (rsIterNumber > 0 ) $ do
366376 let delay = fromMaybe 0 rsPreviousDelay `div` oneSecondInUs
@@ -402,9 +412,25 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThrea
402412 qSchemaCache :: IO (Maybe SchemaCache )
403413 qSchemaCache = do
404414 conf@ AppConfig {.. } <- getConfig appState
415+ -- Throttle concurrent schema cache loads, guarded by advisory locks.
416+ -- This is to prevent thundering herd problem on startup or when many PostgREST
417+ -- instances receive "reload schema" notifications at the same time
418+ -- schema reloading session + listener session
419+ -- See get_lock_sql for details of the algorithm.
420+ -- Here we calculate the number of open connections passed to the query.
421+ Metrics. ConnStats connected inUse <- Metrics. connectionCounts $ connTrack stateMetrics
422+ -- Determine whether schema cache loading will create a new session
423+ let
424+ scLoadingSessions = case (connected <= inUse, inUse >= configDbPoolSize) of
425+ (True , False ) -> 1 -- all connections in use but pool not full - schema cache loading will create session
426+ _ -> 0
427+ withTxLock = SQL. statement
428+ (fromIntegral $ connected + scLoadingSessions)
429+ (SQL. Statement get_lock_sql get_lock_params HD. noResult configDbPreparedStatements)
430+
405431 (resultTime, result) <-
406432 let transaction = if configDbPreparedStatements then SQL. transaction else SQL. unpreparedTransaction in
407- timeItT $ usePool appState (transaction SQL. ReadCommitted SQL. Read $ querySchemaCache conf)
433+ timeItT $ usePool appState (transaction SQL. ReadCommitted SQL. Read $ withTxLock *> querySchemaCache conf)
408434 case result of
409435 Left e -> do
410436 putSCacheStatus appState SCPending
@@ -422,6 +448,43 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThrea
422448 observer $ SchemaCacheLoadedObs t
423449 putSCacheStatus appState SCLoaded
424450 return $ Just sCache
451+ where
452+ -- Recursive query that tries acquiring locks in order
453+ -- and waits for randomly selected lock if no attempt succeeded.
454+ -- It has a single parameter: this node open connection count.
455+ -- It is used to estimate the number of nodes
456+ -- by counting the number of active sessions for current session_user
457+ -- and dividing it by this node open connections.
458+ -- Assuming load is uniform among cluster nodes, all should have
459+ -- statistically the same number of open connections.
460+ -- Once the number of nodes is known we calculate the number
461+ -- of locks as ceil(log(2, number_of_nodes))
462+ get_lock_sql = encodeUtf8 [trimming |
463+ WITH RECURSIVE attempts AS (
464+ SELECT 1 AS lock_number, pg_try_advisory_xact_lock(lock_id, 1) AS success FROM parameters
465+ UNION ALL
466+ SELECT next_lock_number AS lock_number, pg_try_advisory_xact_lock(lock_id, next_lock_number) AS success
467+ FROM
468+ parameters CROSS JOIN LATERAL (
469+ SELECT lock_number + 1 AS next_lock_number FROM attempts
470+ WHERE NOT success AND lock_number < locks_count
471+ ORDER BY lock_number DESC
472+ LIMIT 1
473+ ) AS previous_attempt
474+ ),
475+ counts AS (
476+ SELECT round(log(2, round(count(*)::double precision/$$1)::numeric))::int AS locks_count
477+ FROM
478+ pg_stat_activity WHERE usename = SESSION_USER
479+ ),
480+ parameters AS (
481+ SELECT locks_count, 50168275 AS lock_id FROM counts WHERE locks_count > 0
482+ )
483+ SELECT pg_advisory_xact_lock(lock_id, floor(random() * locks_count)::int + 1)
484+ FROM
485+ parameters WHERE NOT EXISTS (SELECT 1 FROM attempts WHERE success) |]
486+
487+ get_lock_params = HE. param (HE. nonNullable HE. int4)
425488
426489 shouldRetry :: RetryStatus -> (Maybe PgVersion , Maybe SchemaCache ) -> IO Bool
427490 shouldRetry _ (pgVer, sCache) = do
0 commit comments