refactor: encapsulate application termination

This change makes AppState and Listener modules independent from the way how application is terminated. It removes stateMainThreadId from AppState and introduces stateKillApp. It also removes exported  function AppState.getMainThreadId replacing it with more general killApp.
This commit is contained in:
Michał Kłeczek
2026-07-06 10:29:17 -05:00
committed by Steve Chavez
parent 06bda07db1
commit 56df1dc532
5 changed files with 26 additions and 26 deletions
+21 -21
View File
@@ -8,13 +8,13 @@ module PostgREST.AppState
, destroy , destroy
, getConfig , getConfig
, getSchemaCache , getSchemaCache
, getMainThreadId
, getPgVersion , getPgVersion
, getNextDelay , getNextDelay
, getTime , getTime
, getJwtCacheState , getJwtCacheState
, init , init
, initWithPool , initWithPool
, killApp
, putConfig -- For tests TODO refactoring , putConfig -- For tests TODO refactoring
, putSchemaCache , putSchemaCache
, putPgVersion , putPgVersion
@@ -90,7 +90,7 @@ data AppState = AppState
-- | Time used for verifying JWT expiration -- | Time used for verifying JWT expiration
, stateGetTime :: IO UTCTime , stateGetTime :: IO UTCTime
-- | Used for killing the main thread in case a subthread fails -- | Used for killing the main thread in case a subthread fails
, stateMainThreadId :: ThreadId , stateKillApp :: IO ()
-- | Keeps track of the next delay for db connection retry -- | Keeps track of the next delay for db connection retry
, stateNextDelay :: IORef Int , stateNextDelay :: IORef Int
-- | Observation handler -- | Observation handler
@@ -109,8 +109,8 @@ newtype SchemaCacheStatus = SchemaCacheStatus
{ getSCStatusTMVar :: TMVar Bool { getSCStatusTMVar :: TMVar Bool
} }
init :: AppConfig -> IO AppState init :: AppConfig -> IO () -> IO AppState
init conf@AppConfig{configLogLevel, configDbPoolSize} = do init conf@AppConfig{configLogLevel, configDbPoolSize} appKiller = do
loggerState <- Logger.init loggerState <- Logger.init
metricsState <- Metrics.init configDbPoolSize metricsState <- Metrics.init configDbPoolSize
let observer = liftA2 (>>) (Logger.observationLogger loggerState configLogLevel) (Metrics.observationMetrics metricsState) let observer = liftA2 (>>) (Logger.observationLogger loggerState configLogLevel) (Metrics.observationMetrics metricsState)
@@ -118,10 +118,10 @@ init conf@AppConfig{configLogLevel, configDbPoolSize} = do
observer $ AppStartObs prettyVersion observer $ AppStartObs prettyVersion
pool <- initPool conf observer pool <- initPool conf observer
initWithPool pool conf loggerState metricsState observer initWithPool pool conf loggerState metricsState observer appKiller
initWithPool :: SQL.Pool -> AppConfig -> Logger.LoggerState -> Metrics.MetricsState -> ObservationHandler -> IO AppState initWithPool :: SQL.Pool -> AppConfig -> Logger.LoggerState -> Metrics.MetricsState -> ObservationHandler -> IO () -> IO AppState
initWithPool pool conf loggerState metricsState observer = mdo initWithPool pool conf loggerState metricsState observer appKiller = mdo
appState <- AppState pool appState <- AppState pool
<$> newIORef minimumPgVersion -- assume we're in a supported version when starting, this will be corrected on a later step <$> newIORef minimumPgVersion -- assume we're in a supported version when starting, this will be corrected on a later step
@@ -131,7 +131,7 @@ initWithPool pool conf loggerState metricsState observer = mdo
<*> makeDebouncer (retryingSchemaCacheLoad appState *> threadDelay 100000) -- 100ms cooldown <*> makeDebouncer (retryingSchemaCacheLoad appState *> threadDelay 100000) -- 100ms cooldown
<*> newIORef conf <*> newIORef conf
<*> mkAutoUpdate defaultUpdateSettings { updateAction = getCurrentTime } <*> mkAutoUpdate defaultUpdateSettings { updateAction = getCurrentTime }
<*> myThreadId <*> pure appKiller
<*> newIORef 0 <*> newIORef 0
<*> pure observer <*> pure observer
<*> JwtCache.init conf observer <*> JwtCache.init conf observer
@@ -156,7 +156,7 @@ initPool cfg@AppConfig{..} observer = do
-- | 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, stateMainThreadId=mainThreadId, ..} sess = do usePool appState@AppState{stateObserver=observer, ..} sess = do
observer PoolRequest observer PoolRequest
res <- SQL.use statePool sess res <- SQL.use statePool sess
@@ -170,7 +170,7 @@ usePool AppState{stateObserver=observer, stateMainThreadId=mainThreadId, ..} ses
let failureMessage = BS.unpack $ fromMaybe mempty e in let failureMessage = BS.unpack $ fromMaybe mempty e in
when (("FATAL: password authentication failed" `isInfixOf` failureMessage) || ("no password supplied" `isInfixOf` failureMessage)) $ do when (("FATAL: password authentication failed" `isInfixOf` failureMessage) || ("no password supplied" `isInfixOf` failureMessage)) $ do
observer $ ExitDBFatalError ServerAuthError err observer $ ExitDBFatalError ServerAuthError err
killThread mainThreadId killApp appState
err@(SQL.SessionUsageError (SQL.QueryError tpl _ (SQL.ResultError resultErr))) -> err@(SQL.SessionUsageError (SQL.QueryError tpl _ (SQL.ResultError resultErr))) ->
handleResultError err tpl resultErr handleResultError err tpl resultErr
err@(SQL.SessionUsageError (SQL.PipelineError (SQL.ResultError resultErr))) -> err@(SQL.SessionUsageError (SQL.PipelineError (SQL.ResultError resultErr))) ->
@@ -188,32 +188,32 @@ usePool AppState{stateObserver=observer, stateMainThreadId=mainThreadId, ..} ses
case resultErr of case resultErr of
SQL.UnexpectedResult{} -> do SQL.UnexpectedResult{} -> do
observer $ ExitDBFatalError ServerPgrstBug err observer $ ExitDBFatalError ServerPgrstBug err
killThread mainThreadId killApp appState
SQL.RowError{} -> do SQL.RowError{} -> do
observer $ ExitDBFatalError ServerPgrstBug err observer $ ExitDBFatalError ServerPgrstBug err
killThread mainThreadId killApp appState
SQL.UnexpectedAmountOfRows{} -> do SQL.UnexpectedAmountOfRows{} -> do
observer $ ExitDBFatalError ServerPgrstBug err observer $ ExitDBFatalError ServerPgrstBug err
killThread mainThreadId killApp appState
-- Check for a syntax error (42601 is the pg code) only for queries that don't have `WITH pgrst_source` as prefix. -- 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. -- 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 -- TODO have a better way to mark this as a schema cache query
SQL.ServerError "42601" _ _ _ _ -> SQL.ServerError "42601" _ _ _ _ ->
unless ("WITH pgrst_source" `BS.isPrefixOf` tpl) $ do unless ("WITH pgrst_source" `BS.isPrefixOf` tpl) $ do
observer $ ExitDBFatalError ServerPgrstBug err observer $ ExitDBFatalError ServerPgrstBug err
killThread mainThreadId killApp appState
-- Check for a "prepared statement <name> already exists" error (Code 42P05: duplicate_prepared_statement). -- 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 -- This would mean that a connection pooler in transaction mode is being used
-- while prepared statements are enabled in the PostgREST configuration, -- while prepared statements are enabled in the PostgREST configuration,
-- both of which are incompatible with each other. -- both of which are incompatible with each other.
SQL.ServerError "42P05" _ _ _ _ -> do SQL.ServerError "42P05" _ _ _ _ -> do
observer $ ExitDBFatalError ServerError42P05 err observer $ ExitDBFatalError ServerError42P05 err
killThread mainThreadId killApp appState
-- Check for a "transaction blocks not allowed in statement pooling mode" error (Code 08P01: protocol_violation). -- 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. -- 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 SQL.ServerError "08P01" "transaction blocks not allowed in statement pooling mode" _ _ _ -> do
observer $ ExitDBFatalError ServerError08P01 err observer $ ExitDBFatalError ServerError08P01 err
killThread mainThreadId killApp appState
SQL.ServerError{} -> SQL.ServerError{} ->
when (Error.status (Error.PgError False err) >= HTTP.status500) $ when (Error.status (Error.PgError False err) >= HTTP.status500) $
observer $ QueryErrorCodeHighObs err observer $ QueryErrorCodeHighObs err
@@ -261,8 +261,8 @@ getTime = stateGetTime
getJwtCacheState :: AppState -> JwtCacheState getJwtCacheState :: AppState -> JwtCacheState
getJwtCacheState = stateJwtCache getJwtCacheState = stateJwtCache
getMainThreadId :: AppState -> ThreadId killApp :: AppState -> IO ()
getMainThreadId = stateMainThreadId killApp = stateKillApp
isConnEstablished :: AppState -> IO Bool isConnEstablished :: AppState -> IO Bool
isConnEstablished appState = do isConnEstablished appState = do
@@ -298,7 +298,7 @@ getObserver = stateObserver
-- + Because connections cache the pg catalog(see #2620) -- + Because connections cache the pg catalog(see #2620)
-- + For rapid recovery. Otherwise, the pool idle or lifetime timeout would have to be reached for new healthy connections to be acquired. -- + For rapid recovery. Otherwise, the pool idle or lifetime timeout would have to be reached for new healthy connections to be acquired.
retryingSchemaCacheLoad :: AppState -> IO () retryingSchemaCacheLoad :: AppState -> IO ()
retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThreadId=mainThreadId} = retryingSchemaCacheLoad appState@AppState{stateObserver=observer} =
void $ retrying retryPolicy shouldRetry (\RetryStatus{rsIterNumber, rsPreviousDelay} -> do void $ retrying retryPolicy shouldRetry (\RetryStatus{rsIterNumber, rsPreviousDelay} -> do
when (rsIterNumber > 0) $ do when (rsIterNumber > 0) $ do
let delay = fromMaybe 0 rsPreviousDelay `div` oneSecondInUs let delay = fromMaybe 0 rsPreviousDelay `div` oneSecondInUs
@@ -316,12 +316,12 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThrea
observer $ QueryPgVersionError e observer $ QueryPgVersionError e
unless configDbPoolAutomaticRecovery $ do unless configDbPoolAutomaticRecovery $ do
observer ExitDBNoRecoveryObs observer ExitDBNoRecoveryObs
killThread mainThreadId killApp appState
return Nothing return Nothing
Right actualPgVersion -> Right actualPgVersion ->
if actualPgVersion < minimumPgVersion then do if actualPgVersion < minimumPgVersion then do
observer $ ExitUnsupportedPgVersion actualPgVersion minimumPgVersion observer $ ExitUnsupportedPgVersion actualPgVersion minimumPgVersion
killThread mainThreadId killApp appState
return Nothing return Nothing
else do else do
observer $ DBConnectedObs $ pgvFullName actualPgVersion observer $ DBConnectedObs $ pgvFullName actualPgVersion
+2 -1
View File
@@ -42,11 +42,12 @@ runClientCommand conf CmdReady = Client.ready conf
-- | Run postgrest with command -- | Run postgrest with command
runAppCommand :: AppConfig -> RunCommand -> IO () runAppCommand :: AppConfig -> RunCommand -> IO ()
runAppCommand conf@AppConfig{..} runCmd = do runAppCommand conf@AppConfig{..} runCmd = do
mainThreadId <- myThreadId
-- Per https://github.com/PostgREST/postgrest/issues/268, we want to -- Per https://github.com/PostgREST/postgrest/issues/268, we want to
-- explicitly close the connections to PostgreSQL on shutdown. -- explicitly close the connections to PostgreSQL on shutdown.
-- 'AppState.destroy' takes care of that. -- 'AppState.destroy' takes care of that.
bracket bracket
(AppState.init conf) (AppState.init conf (killThread mainThreadId))
AppState.destroy AppState.destroy
(\appState -> case runCmd of (\appState -> case runCmd of
CmdDumpConfig -> do CmdDumpConfig -> do
+1 -2
View File
@@ -48,7 +48,7 @@ retryingListen appState nextDelay hasDbListenerBug = do
when (isDbListenerBug err) $ when (isDbListenerBug err) $
observer DBListenBugCallQueryFix observer DBListenBugCallQueryFix
unless configDbPoolAutomaticRecovery $ unless configDbPoolAutomaticRecovery $
killThread mainThreadId AppState.killApp appState
-- retry the listener -- retry the listener
delay <- readIORef nextDelay delay <- readIORef nextDelay
@@ -96,7 +96,6 @@ retryingListen appState nextDelay hasDbListenerBug = do
exitFailure exitFailure
where where
observer = AppState.getObserver appState observer = AppState.getObserver appState
mainThreadId = AppState.getMainThreadId appState
oneSecondInMicro = 1_000_000 oneSecondInMicro = 1_000_000
maxDelay = 32 maxDelay = 32
+1 -1
View File
@@ -55,7 +55,7 @@ main = do
-- duplicate poolChan as a starting point -- duplicate poolChan as a starting point
obsChan <- dupChan poolChan obsChan <- dupChan poolChan
stateObsChan <- newObsChan obsChan stateObsChan <- newObsChan obsChan
appState <- AppState.initWithPool pool config loggerState metricsState (Metrics.observationMetrics metricsState <> writeChan obsChan) appState <- AppState.initWithPool pool config loggerState metricsState (Metrics.observationMetrics metricsState <> writeChan obsChan) mempty
AppState.putPgVersion appState actualPgVersion AppState.putPgVersion appState actualPgVersion
AppState.putSchemaCache appState (Just sCache) AppState.putSchemaCache appState (Just sCache)
return (SpecState appState metricsState stateObsChan, postgrest appState (pure ())) return (SpecState appState metricsState stateObsChan, postgrest appState (pure ()))
+1 -1
View File
@@ -93,7 +93,7 @@ main = do
let let
initApp sCache config = do initApp sCache config = do
appState <- AppState.initWithPool pool config loggerState metricsState (Metrics.observationMetrics metricsState) appState <- AppState.initWithPool pool config loggerState metricsState (Metrics.observationMetrics metricsState) mempty
AppState.putPgVersion appState actualPgVersion AppState.putPgVersion appState actualPgVersion
AppState.putSchemaCache appState (Just sCache) AppState.putSchemaCache appState (Just sCache)
return ((), postgrest appState (pure ())) return ((), postgrest appState (pure ()))