refactor: move checkIsFatal logic to usePool

The fatal logic is now inside `usePool`. It centralizes the
logic which is better for Locality of Behavior.

Removes:

- The need to do checkIsFatal on other parts of the code
- SCFatalFail/ConnFatalFail states which are no longer needed.
This commit is contained in:
steve-chavez
2024-05-16 17:40:25 -05:00
committed by Steve Chavez
parent 9d763aef00
commit 33b6ba8199
5 changed files with 105 additions and 115 deletions
+1 -1
View File
@@ -65,7 +65,7 @@ run appState = do
let observer = AppState.getObserver appState let observer = AppState.getObserver appState
conf@AppConfig{..} <- AppState.getConfig appState conf@AppConfig{..} <- AppState.getConfig appState
observer $ AppServerStartObs prettyVersion observer $ AppStartObs prettyVersion
AppState.connectionWorker appState -- Loads the initial SchemaCache AppState.connectionWorker appState -- Loads the initial SchemaCache
Unix.installSignalHandlers (AppState.getMainThreadId appState) (AppState.connectionWorker appState) (AppState.reReadConfig False appState) Unix.installSignalHandlers (AppState.getMainThreadId appState) (AppState.connectionWorker appState) (AppState.reReadConfig False appState)
+69 -76
View File
@@ -122,14 +122,12 @@ data AppState = AppState
data SchemaCacheStatus data SchemaCacheStatus
= SCLoaded = SCLoaded
| SCPending | SCPending
| SCFatalFail
deriving Eq deriving Eq
-- | Current database connection status -- | Current database connection status
data ConnectionStatus data ConnectionStatus
= ConnEstablished = ConnEstablished
| ConnPending | ConnPending
| ConnFatalFail Text
deriving Eq deriving Eq
type AppSockets = (NS.Socket, Maybe NS.Socket) type AppSockets = (NS.Socket, Maybe NS.Socket)
@@ -226,15 +224,57 @@ initPool AppConfig{..} observer =
-- | 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)
usePool AppState{stateObserver=observer,..} sess = do usePool AppState{stateObserver=observer, stateMainThreadId=mainThreadId, ..} sess = do
observer PoolRequest
res <- SQL.use statePool sess res <- SQL.use statePool sess
observer PoolRequestFullfilled
whenLeft res (\case whenLeft res (\case
SQL.AcquisitionTimeoutUsageError -> observer $ PoolAcqTimeoutObs SQL.AcquisitionTimeoutUsageError SQL.AcquisitionTimeoutUsageError ->
error observer $ PoolAcqTimeoutObs SQL.AcquisitionTimeoutUsageError
-- TODO We're using the 500 HTTP status for getting all internal db errors but there's no response here. We need a new intermediate type to not rely on the HTTP status. err@(SQL.ConnectionUsageError e) ->
| Error.status (Error.PgError False error) >= HTTP.status500 -> observer $ QueryErrorCodeHighObs error let failureMessage = BS.unpack $ fromMaybe mempty e in
| otherwise -> pure ()) when (("FATAL: password authentication failed" `isInfixOf` failureMessage) || ("no password supplied" `isInfixOf` failureMessage)) $ do
observer $ ExitDBFatalError ServerAuthError err
killThread mainThreadId
err@(SQL.SessionUsageError (SQL.QueryError tpl _ (SQL.ResultError resultErr))) -> do
case resultErr of
SQL.UnexpectedResult{} -> do
observer $ ExitDBFatalError ServerPgrstBug err
killThread mainThreadId
SQL.RowError{} -> do
observer $ ExitDBFatalError ServerPgrstBug err
killThread mainThreadId
SQL.UnexpectedAmountOfRows{} -> do
observer $ ExitDBFatalError ServerPgrstBug err
killThread mainThreadId
-- Check for a syntax error (42601 is the pg code) only for queries that don't have `WITH pgrst_source` as prefix.
-- This would mean the error is on our schema cache queries, so we treat it as fatal.
-- TODO have a better way to mark this as a schema cache query
SQL.ServerError "42601" _ _ _ _ ->
unless ("WITH pgrst_source" `BS.isPrefixOf` tpl) $ do
observer $ ExitDBFatalError ServerPgrstBug err
killThread mainThreadId
-- Check for a "prepared statement <name> already exists" error (Code 42P05: duplicate_prepared_statement).
-- This would mean that a connection pooler in transaction mode is being used
-- while prepared statements are enabled in the PostgREST configuration,
-- both of which are incompatible with each other.
SQL.ServerError "42P05" _ _ _ _ -> do
observer $ ExitDBFatalError ServerError42P05 err
killThread mainThreadId
-- Check for a "transaction blocks not allowed in statement pooling mode" error (Code 08P01: protocol_violation).
-- This would mean that a connection pooler in statement mode is being used which is not supported in PostgREST.
SQL.ServerError "08P01" "transaction blocks not allowed in statement pooling mode" _ _ _ -> do
observer $ ExitDBFatalError ServerError08P01 err
killThread mainThreadId
SQL.ServerError{} ->
when (Error.status (Error.PgError False err) >= HTTP.status500) $
observer $ QueryErrorCodeHighObs err
SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ClientError _)) ->
pure ()
)
return res return res
@@ -340,15 +380,10 @@ loadSchemaCache appState@AppState{stateObserver=observer} = do
timeItT $ usePool appState (transaction SQL.ReadCommitted SQL.Read $ querySchemaCache conf) timeItT $ usePool appState (transaction SQL.ReadCommitted SQL.Read $ querySchemaCache conf)
case result of case result of
Left e -> do Left e -> do
case checkIsFatal e of putSCacheStatus appState SCPending
Just hint -> do putSchemaCache appState Nothing
observer $ SchemaCacheFatalErrorObs e hint observer $ SchemaCacheErrorObs e
return SCFatalFail return SCPending
Nothing -> do
putSCacheStatus appState SCPending
putSchemaCache appState Nothing
observer $ SchemaCacheNormalErrorObs e
return SCPending
Right sCache -> do Right sCache -> do
-- IMPORTANT: While the pending schema cache state starts from running the above querySchemaCache, only at this stage we block API requests due to the usage of an -- IMPORTANT: While the pending schema cache state starts from running the above querySchemaCache, only at this stage we block API requests due to the usage of an
@@ -373,24 +408,25 @@ loadSchemaCache appState@AppState{stateObserver=observer} = do
-- program. -- program.
-- 3. Obtains the sCache. If this fails, it goes back to 1. -- 3. Obtains the sCache. If this fails, it goes back to 1.
internalConnectionWorker :: AppState -> IO () internalConnectionWorker :: AppState -> IO ()
internalConnectionWorker appState@AppState{stateObserver=observer} = work internalConnectionWorker appState@AppState{stateObserver=observer, stateMainThreadId=mainThreadId} = work
where where
work = do work = do
AppConfig{..} <- getConfig appState AppConfig{..} <- getConfig appState
observer DBConnectAttemptObs observer DBConnectAttemptObs
connStatus <- establishConnection appState connStatus <- establishConnection appState
case connStatus of case connStatus of
ConnFatalFail reason ->
-- Fatal error when connecting
observer (ExitFatalObs reason) >> killThread (getMainThreadId appState)
ConnPending -> ConnPending ->
unless configDbPoolAutomaticRecovery unless configDbPoolAutomaticRecovery $ do
$ observer ExitDBNoRecoveryObs >> killThread (getMainThreadId appState) observer ExitDBNoRecoveryObs
killThread mainThreadId
ConnEstablished -> do ConnEstablished -> do
actualPgVersion <- getPgVersion appState
when (actualPgVersion < minimumPgVersion) $ do
observer $ ExitUnsupportedPgVersion actualPgVersion minimumPgVersion
killThread mainThreadId
-- Procede with initialization -- Procede with initialization
when configDbChannelEnabled $ when configDbChannelEnabled $
signalListener appState signalListener appState
actualPgVersion <- getPgVersion appState
observer (DBConnectedObs $ pgvFullName actualPgVersion) observer (DBConnectedObs $ pgvFullName actualPgVersion)
-- this could be fail because the connection drops, but the loadSchemaCache will pick the error and retry again -- this could be fail because the connection drops, but the loadSchemaCache will pick the error and retry again
-- We cannot retry after it fails immediately, because db-pre-config could have user errors. We just log the error and continue. -- We cannot retry after it fails immediately, because db-pre-config could have user errors. We just log the error and continue.
@@ -403,9 +439,6 @@ internalConnectionWorker appState@AppState{stateObserver=observer} = work
SCPending -> SCPending ->
-- retry reloading the schema cache -- retry reloading the schema cache
work work
SCFatalFail ->
-- die if our schema cache query has an error
killThread $ getMainThreadId appState
-- | Repeatedly flush the pool, and check if a connection from the -- | Repeatedly flush the pool, and check if a connection from the
-- pool allows access to the PostgreSQL database. -- pool allows access to the PostgreSQL database.
@@ -432,21 +465,12 @@ establishConnection appState@AppState{stateObserver=observer} =
case pgVersion of case pgVersion of
Left e -> do Left e -> do
observer $ ConnectionPgVersionErrorObs e observer $ ConnectionPgVersionErrorObs e
case checkIsFatal e of putConnStatus appState ConnPending
Just reason -> return ConnPending
return $ ConnFatalFail reason Right version -> do
Nothing -> do putConnStatus appState ConnEstablished
putConnStatus appState ConnPending putPgVersion appState version
return ConnPending return ConnEstablished
Right version ->
if version < minimumPgVersion then
return . ConnFatalFail $
"Cannot run in this PostgreSQL version, PostgREST needs at least "
<> pgvName minimumPgVersion
else do
putConnStatus appState ConnEstablished
putPgVersion appState version
return ConnEstablished
shouldRetry :: RetryStatus -> ConnectionStatus -> IO Bool shouldRetry :: RetryStatus -> ConnectionStatus -> IO Bool
shouldRetry rs isConnSucc = do shouldRetry rs isConnSucc = do
@@ -468,13 +492,7 @@ reReadConfig startingUp appState@AppState{stateObserver=observer} = do
qDbSettings <- usePool appState (queryDbSettings (dumpQi <$> configDbPreConfig) configDbPreparedStatements) qDbSettings <- usePool appState (queryDbSettings (dumpQi <$> configDbPreConfig) configDbPreparedStatements)
case qDbSettings of case qDbSettings of
Left e -> do Left e -> do
observer ConfigReadErrorObs observer $ ConfigReadErrorObs e
case checkIsFatal e of
Just hint -> do
observer $ ConfigReadErrorFatalObs e hint
killThread (getMainThreadId appState)
Nothing -> do
observer $ ConfigReadErrorNotFatalObs e
pure mempty pure mempty
Right x -> pure x Right x -> pure x
else else
@@ -510,7 +528,7 @@ runListener conf@AppConfig{configDbChannelEnabled} appState = do
-- NOTIFY <db-channel> - with an empty payload - is done, it refills the schema -- NOTIFY <db-channel> - with an empty payload - is done, it refills the schema
-- cache. It uses the connectionWorker in case the LISTEN connection dies. -- cache. It uses the connectionWorker in case the LISTEN connection dies.
listener :: AppState -> AppConfig -> IO () listener :: AppState -> AppConfig -> IO ()
listener appState@AppState{stateObserver=observer} conf@AppConfig{..} = do listener appState@AppState{stateObserver=observer, stateMainThreadId=mainThreadId} conf@AppConfig{..} = do
let dbChannel = toS configDbChannel let dbChannel = toS configDbChannel
-- The listener has to wait for a signal from the connectionWorker. -- The listener has to wait for a signal from the connectionWorker.
@@ -534,7 +552,7 @@ listener appState@AppState{stateObserver=observer} conf@AppConfig{..} = do
where where
handleFinally dbChannel False err = do handleFinally dbChannel False err = do
observer $ DBListenerFailRecoverObs False dbChannel err observer $ DBListenerFailRecoverObs False dbChannel err
killThread (getMainThreadId appState) killThread mainThreadId
handleFinally dbChannel True err = do handleFinally dbChannel True err = do
-- if the thread dies, we try to recover -- if the thread dies, we try to recover
observer $ DBListenerFailRecoverObs True dbChannel err observer $ DBListenerFailRecoverObs True dbChannel err
@@ -554,28 +572,3 @@ listener appState@AppState{stateObserver=observer} conf@AppConfig{..} = do
-- it's necessary to restart the pg connections because they cache the pg catalog(see #2620) -- it's necessary to restart the pg connections because they cache the pg catalog(see #2620)
connectionWorker appState connectionWorker appState
checkIsFatal :: SQL.UsageError -> Maybe Text
checkIsFatal (SQL.ConnectionUsageError e)
| isAuthFailureMessage = Just $ toS failureMessage
| otherwise = Nothing
where isAuthFailureMessage =
("FATAL: password authentication failed" `isInfixOf` failureMessage) ||
("no password supplied" `isInfixOf` failureMessage)
failureMessage = BS.unpack $ fromMaybe mempty e
checkIsFatal(SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ResultError serverError)))
= case serverError of
-- Check for a syntax error (42601 is the pg code). This would mean the error is on our part somehow, so we treat it as fatal.
SQL.ServerError "42601" _ _ _ _
-> Just "This is probably a bug in PostgREST, please report it at https://github.com/PostgREST/postgrest/issues"
-- Check for a "prepared statement <name> already exists" error (Code 42P05: duplicate_prepared_statement).
-- This would mean that a connection pooler in transaction mode is being used
-- while prepared statements are enabled in the PostgREST configuration,
-- both of which are incompatible with each other.
SQL.ServerError "42P05" _ _ _ _
-> Just "If you are using connection poolers in transaction mode, try setting db-prepared-statements to false."
-- Check for a "transaction blocks not allowed in statement pooling mode" error (Code 08P01: protocol_violation).
-- This would mean that a connection pooler in statement mode is being used which is not supported in PostgREST.
SQL.ServerError "08P01" "transaction blocks not allowed in statement pooling mode" _ _ _
-> Just "Connection poolers in statement mode are not supported."
_ -> Nothing
checkIsFatal _ = Nothing
+1 -1
View File
@@ -47,7 +47,7 @@ observationMetrics (MetricsState poolTimeouts poolAvailable poolWaiting _ schema
SchemaCacheLoadedObs resTime -> do SchemaCacheLoadedObs resTime -> do
withLabel schemaCacheLoads "SUCCESS" incCounter withLabel schemaCacheLoads "SUCCESS" incCounter
setGauge schemaCacheQueryTime resTime setGauge schemaCacheQueryTime resTime
SchemaCacheNormalErrorObs _ -> do SchemaCacheErrorObs _ -> do
withLabel schemaCacheLoads "FAIL" incCounter withLabel schemaCacheLoads "FAIL" incCounter
_ -> _ ->
pure () pure ()
+34 -30
View File
@@ -5,34 +5,36 @@ Description : Module for observability types
-} -}
module PostgREST.Observation module PostgREST.Observation
( Observation(..) ( Observation(..)
, ObsFatalError(..)
, observationMessage , observationMessage
, ObservationHandler , ObservationHandler
) where ) where
import qualified Data.ByteString.Lazy as LBS import qualified Data.ByteString.Lazy as LBS
import qualified Data.Text as T import qualified Data.Text as T
import qualified Data.Text.Encoding as T import qualified Data.Text.Encoding as T
import qualified Hasql.Connection as SQL import qualified Hasql.Connection as SQL
import qualified Hasql.Pool as SQL import qualified Hasql.Pool as SQL
import qualified Hasql.Pool.Observation as SQL import qualified Hasql.Pool.Observation as SQL
import qualified Network.Socket as NS import qualified Network.Socket as NS
import Numeric (showFFloat) import Numeric (showFFloat)
import qualified PostgREST.Error as Error import PostgREST.Config.PgVersion
import qualified PostgREST.Error as Error
import Protolude import Protolude
import Protolude.Partial (fromJust) import Protolude.Partial (fromJust)
data Observation data Observation
= AdminStartObs (Maybe Int) = AdminStartObs (Maybe Int)
| AppServerStartObs ByteString | AppStartObs ByteString
| AppServerPortObs NS.PortNumber | AppServerPortObs NS.PortNumber
| AppServerUnixObs FilePath | AppServerUnixObs FilePath
| DBConnectAttemptObs | DBConnectAttemptObs
| ExitFatalObs Text | ExitUnsupportedPgVersion PgVersion PgVersion
| ExitDBNoRecoveryObs | ExitDBNoRecoveryObs
| ExitDBFatalError ObsFatalError SQL.UsageError
| DBConnectedObs Text | DBConnectedObs Text
| SchemaCacheFatalErrorObs SQL.UsageError Text | SchemaCacheErrorObs SQL.UsageError
| SchemaCacheNormalErrorObs SQL.UsageError
| SchemaCacheQueriedObs Double | SchemaCacheQueriedObs Double
| SchemaCacheSummaryObs Text | SchemaCacheSummaryObs Text
| SchemaCacheLoadedObs Double | SchemaCacheLoadedObs Double
@@ -43,9 +45,7 @@ data Observation
| DBListenerFailRecoverObs Bool Text (Either SomeException ()) | DBListenerFailRecoverObs Bool Text (Either SomeException ())
| DBListenerGotSCacheMsg ByteString | DBListenerGotSCacheMsg ByteString
| DBListenerGotConfigMsg ByteString | DBListenerGotConfigMsg ByteString
| ConfigReadErrorObs | ConfigReadErrorObs SQL.UsageError
| ConfigReadErrorFatalObs SQL.UsageError Text
| ConfigReadErrorNotFatalObs SQL.UsageError
| ConfigInvalidObs Text | ConfigInvalidObs Text
| ConfigSucceededObs | ConfigSucceededObs
| QueryRoleSettingsErrorObs SQL.UsageError | QueryRoleSettingsErrorObs SQL.UsageError
@@ -55,13 +55,15 @@ data Observation
| PoolRequest | PoolRequest
| PoolRequestFullfilled | PoolRequestFullfilled
data ObsFatalError = ServerAuthError | ServerPgrstBug | ServerError42P05 | ServerError08P01
type ObservationHandler = Observation -> IO () type ObservationHandler = Observation -> IO ()
observationMessage :: Observation -> Text observationMessage :: Observation -> Text
observationMessage = \case observationMessage = \case
AdminStartObs port -> AdminStartObs port ->
"Admin server listening on port " <> show (fromIntegral (fromJust port) :: Integer) "Admin server listening on port " <> show (fromIntegral (fromJust port) :: Integer)
AppServerStartObs ver -> AppStartObs ver ->
"Starting PostgREST " <> T.decodeUtf8 ver <> "..." "Starting PostgREST " <> T.decodeUtf8 ver <> "..."
AppServerPortObs port -> AppServerPortObs port ->
"Listening on port " <> show port "Listening on port " <> show port
@@ -69,15 +71,21 @@ observationMessage = \case
"Listening on unix socket " <> show sock "Listening on unix socket " <> show sock
DBConnectAttemptObs -> DBConnectAttemptObs ->
"Attempting to connect to the database..." "Attempting to connect to the database..."
ExitFatalObs reason ->
"Fatal error encountered. " <> reason
ExitDBNoRecoveryObs ->
"Automatic recovery disabled, exiting."
DBConnectedObs ver -> DBConnectedObs ver ->
"Successfully connected to " <> ver "Successfully connected to " <> ver
SchemaCacheFatalErrorObs usageErr hint -> ExitUnsupportedPgVersion pgVer minPgVer ->
"A fatal error ocurred when loading the schema cache. " <> hint <> ". " <> jsonMessage usageErr "Cannot run in this PostgreSQL version (" <> pgvName pgVer <> "), PostgREST needs at least " <> pgvName minPgVer
SchemaCacheNormalErrorObs usageErr -> ExitDBNoRecoveryObs ->
"Automatic recovery disabled, exiting."
ExitDBFatalError ServerAuthError usageErr ->
jsonMessage usageErr
ExitDBFatalError ServerPgrstBug usageErr ->
"This is probably a bug in PostgREST, please report it at https://github.com/PostgREST/postgrest/issues. " <> jsonMessage usageErr
ExitDBFatalError ServerError42P05 usageErr ->
"If you are using connection poolers in transaction mode, try setting db-prepared-statements to false. " <> jsonMessage usageErr
ExitDBFatalError ServerError08P01 usageErr ->
"Connection poolers in statement mode are not supported." <> jsonMessage usageErr
SchemaCacheErrorObs usageErr ->
"An error ocurred when loading the schema cache. " <> jsonMessage usageErr "An error ocurred when loading the schema cache. " <> jsonMessage usageErr
SchemaCacheQueriedObs resultTime -> SchemaCacheQueriedObs resultTime ->
"Schema cache queried in " <> showMillis resultTime <> " milliseconds" "Schema cache queried in " <> showMillis resultTime <> " milliseconds"
@@ -99,12 +107,8 @@ observationMessage = \case
"Received a schema cache reload message on the " <> show channel <> " channel" "Received a schema cache reload message on the " <> show channel <> " channel"
DBListenerGotConfigMsg channel -> DBListenerGotConfigMsg channel ->
"Received a config reload message on the " <> show channel <> " channel" "Received a config reload message on the " <> show channel <> " channel"
ConfigReadErrorObs -> ConfigReadErrorObs usageErr ->
"An error ocurred when trying to query database settings for the config parameters" "An error ocurred when trying to query database settings for the config parameters." <> jsonMessage usageErr
ConfigReadErrorFatalObs usageErr hint ->
hint <> ". " <> jsonMessage usageErr
ConfigReadErrorNotFatalObs usageErr ->
jsonMessage usageErr
QueryRoleSettingsErrorObs usageErr -> QueryRoleSettingsErrorObs usageErr ->
"An error ocurred when trying to query the role settings. " <> jsonMessage usageErr "An error ocurred when trying to query the role settings. " <> jsonMessage usageErr
QueryErrorCodeHighObs usageErr -> QueryErrorCodeHighObs usageErr ->
-7
View File
@@ -43,7 +43,6 @@ import PostgREST.Config (AppConfig (..),
import PostgREST.Config.PgVersion (PgVersion (..)) import PostgREST.Config.PgVersion (PgVersion (..))
import PostgREST.Error (Error) import PostgREST.Error (Error)
import PostgREST.MediaType (MediaType (..)) import PostgREST.MediaType (MediaType (..))
import PostgREST.Observation (Observation (..))
import PostgREST.Plan (ActionPlan (..), import PostgREST.Plan (ActionPlan (..),
CallReadPlan (..), CallReadPlan (..),
CrudPlan (..), CrudPlan (..),
@@ -78,16 +77,10 @@ data QueryResult
runQuery :: AppState.AppState -> AppConfig -> AuthResult -> ApiRequest -> ActionPlan -> SchemaCache -> PgVersion -> Bool -> ExceptT Error IO QueryResult runQuery :: AppState.AppState -> AppConfig -> AuthResult -> ApiRequest -> ActionPlan -> SchemaCache -> PgVersion -> Bool -> ExceptT Error IO QueryResult
runQuery _ _ _ _ (NoDb x) _ _ _ = pure $ NoDbResult x runQuery _ _ _ _ (NoDb x) _ _ _ = pure $ NoDbResult x
runQuery appState config AuthResult{..} apiReq (Db plan) sCache pgVer authenticated = do runQuery appState config AuthResult{..} apiReq (Db plan) sCache pgVer authenticated = do
let observer = AppState.getObserver appState
lift $ observer PoolRequest
dbResp <- lift $ do dbResp <- lift $ do
let transaction = if prepared then SQL.transaction else SQL.unpreparedTransaction let transaction = if prepared then SQL.transaction else SQL.unpreparedTransaction
AppState.usePool appState (transaction isoLvl txMode $ runExceptT dbHandler) AppState.usePool appState (transaction isoLvl txMode $ runExceptT dbHandler)
lift $ observer PoolRequestFullfilled
resp <- resp <-
liftEither . mapLeft Error.PgErr $ liftEither . mapLeft Error.PgErr $
mapLeft (Error.PgError authenticated) dbResp mapLeft (Error.PgError authenticated) dbResp