feat: introduce pool acquisition timeout (fixes #2348)

The configuration option db-pool-acquisition-timeout
specifies the time in seconds to wait for the pool to
free up a connection slot. Otherwise, a 504 error is
returned. By default, there is no timeout.
This commit is contained in:
Robert Vollmert
2022-08-31 19:54:34 +02:00
committed by Robert
parent 554db21f49
commit ba1fcfd1e3
16 changed files with 72 additions and 35 deletions
+1
View File
@@ -8,6 +8,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
### Fixed ### Fixed
- #2401, #2444, Fix SIGUSR1 to fully flush connections pool, remove `db-pool-timeout`. - @robx - #2401, #2444, Fix SIGUSR1 to fully flush connections pool, remove `db-pool-timeout`. - @robx
- #2348, Add `db-pool-acquisition-timeout` configuration option, time in seconds to wait to acquire a connection. - @robx
### Deprecated ### Deprecated
+4 -1
View File
@@ -97,7 +97,10 @@ destroy = destroyPool
initPool :: AppConfig -> IO SQL.Pool initPool :: AppConfig -> IO SQL.Pool
initPool AppConfig{..} = initPool AppConfig{..} =
SQL.acquire configDbPoolSize Nothing $ toUtf8 configDbUri SQL.acquire configDbPoolSize timeoutMilliseconds $ toUtf8 configDbUri
where
timeoutMilliseconds = (* oneSecond) <$> configDbPoolAcquisitionTimeout
oneSecond = 1000000
-- | Run an action with a database connection. -- | Run an action with a database connection.
usePool :: AppState -> SQL.Session a -> IO (Either SQL.UsageError a) usePool :: AppState -> SQL.Session a -> IO (Either SQL.UsageError a)
+3
View File
@@ -148,6 +148,9 @@ exampleConfigFile =
|## Number of open connections in the pool |## Number of open connections in the pool
|db-pool = 10 |db-pool = 10
| |
|## Time in seconds to wait to acquire a slot from the connection pool
|# db-pool-acquisition-timeout = 10
|
|## Stored proc to exec immediately after auth |## Stored proc to exec immediately after auth
|# db-pre-request = "stored_proc_name" |# db-pre-request = "stored_proc_name"
| |
+37 -34
View File
@@ -62,39 +62,40 @@ import Protolude hiding (Proxy, toList)
data AppConfig = AppConfig data AppConfig = AppConfig
{ configAppSettings :: [(Text, Text)] { configAppSettings :: [(Text, Text)]
, configDbAnonRole :: Maybe Text , configDbAnonRole :: Maybe Text
, configDbChannel :: Text , configDbChannel :: Text
, configDbChannelEnabled :: Bool , configDbChannelEnabled :: Bool
, configDbExtraSearchPath :: [Text] , configDbExtraSearchPath :: [Text]
, configDbMaxRows :: Maybe Integer , configDbMaxRows :: Maybe Integer
, configDbPlanEnabled :: Bool , configDbPlanEnabled :: Bool
, configDbPoolSize :: Int , configDbPoolSize :: Int
, configDbPreRequest :: Maybe QualifiedIdentifier , configDbPoolAcquisitionTimeout :: Maybe Int
, configDbPreparedStatements :: Bool , configDbPreRequest :: Maybe QualifiedIdentifier
, configDbRootSpec :: Maybe QualifiedIdentifier , configDbPreparedStatements :: Bool
, configDbSchemas :: NonEmpty Text , configDbRootSpec :: Maybe QualifiedIdentifier
, configDbConfig :: Bool , configDbSchemas :: NonEmpty Text
, configDbTxAllowOverride :: Bool , configDbConfig :: Bool
, configDbTxRollbackAll :: Bool , configDbTxAllowOverride :: Bool
, configDbUri :: Text , configDbTxRollbackAll :: Bool
, configDbUseLegacyGucs :: Bool , configDbUri :: Text
, configFilePath :: Maybe FilePath , configDbUseLegacyGucs :: Bool
, configJWKS :: Maybe JWKSet , configFilePath :: Maybe FilePath
, configJwtAudience :: Maybe StringOrURI , configJWKS :: Maybe JWKSet
, configJwtRoleClaimKey :: JSPath , configJwtAudience :: Maybe StringOrURI
, configJwtSecret :: Maybe BS.ByteString , configJwtRoleClaimKey :: JSPath
, configJwtSecretIsBase64 :: Bool , configJwtSecret :: Maybe BS.ByteString
, configLogLevel :: LogLevel , configJwtSecretIsBase64 :: Bool
, configOpenApiMode :: OpenAPIMode , configLogLevel :: LogLevel
, configOpenApiSecurityActive :: Bool , configOpenApiMode :: OpenAPIMode
, configOpenApiServerProxyUri :: Maybe Text , configOpenApiSecurityActive :: Bool
, configRawMediaTypes :: [MediaType] , configOpenApiServerProxyUri :: Maybe Text
, configServerHost :: Text , configRawMediaTypes :: [MediaType]
, configServerPort :: Int , configServerHost :: Text
, configServerUnixSocket :: Maybe FilePath , configServerPort :: Int
, configServerUnixSocketMode :: FileMode , configServerUnixSocket :: Maybe FilePath
, configAdminServerPort :: Maybe Int , configServerUnixSocketMode :: FileMode
, configAdminServerPort :: Maybe Int
} }
data LogLevel = LogCrit | LogError | LogWarn | LogInfo data LogLevel = LogCrit | LogError | LogWarn | LogInfo
@@ -129,6 +130,7 @@ toText conf =
,("db-max-rows", maybe "\"\"" show . configDbMaxRows) ,("db-max-rows", maybe "\"\"" show . configDbMaxRows)
,("db-plan-enabled", T.toLower . show . configDbPlanEnabled) ,("db-plan-enabled", T.toLower . show . configDbPlanEnabled)
,("db-pool", show . configDbPoolSize) ,("db-pool", show . configDbPoolSize)
,("db-pool-acquisition-timeout", maybe "\"\"" show . configDbPoolAcquisitionTimeout)
,("db-pre-request", q . maybe mempty dumpQi . configDbPreRequest) ,("db-pre-request", q . maybe mempty dumpQi . configDbPreRequest)
,("db-prepared-statements", T.toLower . show . configDbPreparedStatements) ,("db-prepared-statements", T.toLower . show . configDbPreparedStatements)
,("db-root-spec", q . maybe mempty dumpQi . configDbRootSpec) ,("db-root-spec", q . maybe mempty dumpQi . configDbRootSpec)
@@ -217,6 +219,7 @@ parser optPath env dbSettings =
(optInt "max-rows") (optInt "max-rows")
<*> (fromMaybe False <$> optBool "db-plan-enabled") <*> (fromMaybe False <$> optBool "db-plan-enabled")
<*> (fromMaybe 10 <$> optInt "db-pool") <*> (fromMaybe 10 <$> optInt "db-pool")
<*> optInt "db-pool-acquisition-timeout"
<*> (fmap toQi <$> optWithAlias (optString "db-pre-request") <*> (fmap toQi <$> optWithAlias (optString "db-pre-request")
(optString "pre-request")) (optString "pre-request"))
<*> (fromMaybe True <$> optBool "db-prepared-statements") <*> (fromMaybe True <$> optBool "db-prepared-statements")
@@ -352,7 +355,7 @@ parser optPath env dbSettings =
let dbSettingName = T.pack $ dashToUnderscore <$> toS key in let dbSettingName = T.pack $ dashToUnderscore <$> toS key in
if dbSettingName `notElem` [ if dbSettingName `notElem` [
"server_host", "server_port", "server_unix_socket", "server_unix_socket_mode", "admin_server_port", "log_level", "server_host", "server_port", "server_unix_socket", "server_unix_socket_mode", "admin_server_port", "log_level",
"db_uri", "db_channel_enabled", "db_channel", "db_pool", "db_config"] "db_uri", "db_channel_enabled", "db_channel", "db_pool", "db_pool_acquisition_timeout", "db_config"]
then lookup dbSettingName dbSettings then lookup dbSettingName dbSettings
else Nothing else Nothing
+1
View File
@@ -5,6 +5,7 @@ db-extra-search-path = "public"
db-max-rows = 1000 db-max-rows = 1000
db-plan-enabled = false db-plan-enabled = false
db-pool = 10 db-pool = 10
db-pool-acquisition-timeout = ""
db-pre-request = "check_alias" db-pre-request = "check_alias"
db-prepared-statements = true db-prepared-statements = true
db-root-spec = "open_alias" db-root-spec = "open_alias"
@@ -5,6 +5,7 @@ db-extra-search-path = "public"
db-max-rows = "" db-max-rows = ""
db-plan-enabled = false db-plan-enabled = false
db-pool = 10 db-pool = 10
db-pool-acquisition-timeout = ""
db-pre-request = "" db-pre-request = ""
db-prepared-statements = false db-prepared-statements = false
db-root-spec = "" db-root-spec = ""
@@ -5,6 +5,7 @@ db-extra-search-path = "public"
db-max-rows = "" db-max-rows = ""
db-plan-enabled = false db-plan-enabled = false
db-pool = 10 db-pool = 10
db-pool-acquisition-timeout = ""
db-pre-request = "" db-pre-request = ""
db-prepared-statements = false db-prepared-statements = false
db-root-spec = "" db-root-spec = ""
+1
View File
@@ -5,6 +5,7 @@ db-extra-search-path = "public"
db-max-rows = "" db-max-rows = ""
db-plan-enabled = false db-plan-enabled = false
db-pool = 10 db-pool = 10
db-pool-acquisition-timeout = ""
db-pre-request = "" db-pre-request = ""
db-prepared-statements = true db-prepared-statements = true
db-root-spec = "" db-root-spec = ""
@@ -5,6 +5,7 @@ db-extra-search-path = "public,extensions,other"
db-max-rows = 100 db-max-rows = 100
db-plan-enabled = true db-plan-enabled = true
db-pool = 1 db-pool = 1
db-pool-acquisition-timeout = 10
db-pre-request = "test.other_custom_headers" db-pre-request = "test.other_custom_headers"
db-prepared-statements = false db-prepared-statements = false
db-root-spec = "other_root" db-root-spec = "other_root"
@@ -5,6 +5,7 @@ db-extra-search-path = "public,extensions,private"
db-max-rows = 1000 db-max-rows = 1000
db-plan-enabled = true db-plan-enabled = true
db-pool = 1 db-pool = 1
db-pool-acquisition-timeout = 10
db-pre-request = "test.custom_headers" db-pre-request = "test.custom_headers"
db-prepared-statements = false db-prepared-statements = false
db-root-spec = "root" db-root-spec = "root"
@@ -5,6 +5,7 @@ db-extra-search-path = "public,test"
db-max-rows = 1000 db-max-rows = 1000
db-plan-enabled = true db-plan-enabled = true
db-pool = 1 db-pool = 1
db-pool-acquisition-timeout = 10
db-pre-request = "please_run_fast" db-pre-request = "please_run_fast"
db-prepared-statements = false db-prepared-statements = false
db-root-spec = "openapi_v3" db-root-spec = "openapi_v3"
+1
View File
@@ -5,6 +5,7 @@ db-extra-search-path = "public"
db-max-rows = "" db-max-rows = ""
db-plan-enabled = false db-plan-enabled = false
db-pool = 10 db-pool = 10
db-pool-acquisition-timeout = ""
db-pre-request = "" db-pre-request = ""
db-prepared-statements = true db-prepared-statements = true
db-root-spec = "" db-root-spec = ""
+1
View File
@@ -7,6 +7,7 @@ PGRST_DB_EXTRA_SEARCH_PATH: public, test
PGRST_DB_MAX_ROWS: 1000 PGRST_DB_MAX_ROWS: 1000
PGRST_DB_PLAN_ENABLED: true PGRST_DB_PLAN_ENABLED: true
PGRST_DB_POOL: 1 PGRST_DB_POOL: 1
PGRST_DB_POOL_ACQUISITION_TIMEOUT: 10
PGRST_DB_PREPARED_STATEMENTS: false PGRST_DB_PREPARED_STATEMENTS: false
PGRST_DB_PRE_REQUEST: please_run_fast PGRST_DB_PRE_REQUEST: please_run_fast
PGRST_DB_ROOT_SPEC: openapi_v3 PGRST_DB_ROOT_SPEC: openapi_v3
+1
View File
@@ -5,6 +5,7 @@ db-extra-search-path = "public, test"
db-max-rows = 1000 db-max-rows = 1000
db-plan-enabled = true db-plan-enabled = true
db-pool = 1 db-pool = 1
db-pool-acquisition-timeout = 10
db-pre-request = "please_run_fast" db-pre-request = "please_run_fast"
db-prepared-statements = false db-prepared-statements = false
db-root-spec = "openapi_v3" db-root-spec = "openapi_v3"
+16
View File
@@ -548,6 +548,22 @@ def test_pool_size(defaultenv, metapostgrest):
assert delta > 1 and delta < 1.5 assert delta > 1 and delta < 1.5
def test_pool_acquisition_timeout(defaultenv, metapostgrest):
"Verify that PGRST_DB_POOL_ACQUISITON_TIMEOUT times out when the pool is empty"
env = {
**defaultenv,
"PGRST_DB_POOL": "1",
"PGRST_DB_POOL_ACQUISITION_TIMEOUT": "1", # 1 second
}
with run(env=env, no_pool_connection_available=True) as postgrest:
response = postgrest.session.get("/projects")
assert response.status_code == 504
data = response.json()
assert data["message"] == "Timed out acquiring connection from connection pool."
def test_change_statement_timeout_held_connection(defaultenv, metapostgrest): def test_change_statement_timeout_held_connection(defaultenv, metapostgrest):
"Statement timeout changes take effect immediately, even with a request outliving the reconfiguration" "Statement timeout changes take effect immediately, even with a request outliving the reconfiguration"
+1
View File
@@ -79,6 +79,7 @@ baseCfg = let secret = Just $ encodeUtf8 "reallyreallyreallyreallyverysafe" in
, configDbMaxRows = Nothing , configDbMaxRows = Nothing
, configDbPlanEnabled = False , configDbPlanEnabled = False
, configDbPoolSize = 10 , configDbPoolSize = 10
, configDbPoolAcquisitionTimeout = Nothing
, configDbPreRequest = Just $ QualifiedIdentifier "test" "switch_role" , configDbPreRequest = Just $ QualifiedIdentifier "test" "switch_role"
, configDbPreparedStatements = True , configDbPreparedStatements = True
, configDbRootSpec = Nothing , configDbRootSpec = Nothing