refactor: is ready Admin logic to AppState

This commit is contained in:
steve-chavez
2024-05-08 11:27:58 -05:00
committed by Steve Chavez
parent 1374178f27
commit 1b584f7e9c
3 changed files with 69 additions and 63 deletions
+8 -12
View File
@@ -5,7 +5,6 @@ module PostgREST.Admin
) where ) where
import qualified Data.Aeson as JSON import qualified Data.Aeson as JSON
import qualified Hasql.Session as SQL
import qualified Network.HTTP.Types.Status as HTTP import qualified Network.HTTP.Types.Status as HTTP
import qualified Network.Wai as Wai import qualified Network.Wai as Wai
import qualified Network.Wai.Handler.Warp as Warp import qualified Network.Wai.Handler.Warp as Warp
@@ -28,28 +27,25 @@ import qualified PostgREST.Config as Config
import Protolude import Protolude
runAdmin :: AppConfig -> AppState -> Warp.Settings -> IO () runAdmin :: AppState -> Warp.Settings -> IO ()
runAdmin conf@AppConfig{configAdminServerPort} appState settings = runAdmin appState settings = do
AppConfig{configAdminServerPort} <- AppState.getConfig appState
whenJust (AppState.getSocketAdmin appState) $ \adminSocket -> do whenJust (AppState.getSocketAdmin appState) $ \adminSocket -> do
observer $ AdminStartObs configAdminServerPort observer $ AdminStartObs configAdminServerPort
void . forkIO $ Warp.runSettingsSocket settings adminSocket adminApp void . forkIO $ Warp.runSettingsSocket settings adminSocket adminApp
where where
adminApp = admin appState conf adminApp = admin appState
observer = AppState.getObserver appState observer = AppState.getObserver appState
-- | PostgREST admin application -- | PostgREST admin application
admin :: AppState.AppState -> AppConfig -> Wai.Application admin :: AppState.AppState -> Wai.Application
admin appState appConfig req respond = do admin appState req respond = do
isMainAppReachable <- isRight <$> reachMainApp (AppState.getSocketREST appState) isMainAppReachable <- isRight <$> reachMainApp (AppState.getSocketREST appState)
isSchemaCacheLoaded <- AppState.getSchemaCacheLoaded appState isLoaded <- AppState.isLoaded appState
isConnectionUp <-
if configDbChannelEnabled appConfig
then AppState.getIsListenerOn appState
else isRight <$> AppState.usePool appState (SQL.sql "SELECT 1")
case Wai.pathInfo req of case Wai.pathInfo req of
["ready"] -> ["ready"] ->
respond $ Wai.responseLBS (if isMainAppReachable && isConnectionUp && isSchemaCacheLoaded then HTTP.status200 else HTTP.status503) [] mempty respond $ Wai.responseLBS (if isMainAppReachable && isLoaded then HTTP.status200 else HTTP.status503) [] mempty
["live"] -> ["live"] ->
respond $ Wai.responseLBS (if isMainAppReachable then HTTP.status200 else HTTP.status503) [] mempty respond $ Wai.responseLBS (if isMainAppReachable then HTTP.status200 else HTTP.status503) [] mempty
["config"] -> do ["config"] -> do
+1 -1
View File
@@ -72,7 +72,7 @@ run appState = do
-- reload schema cache + config on NOTIFY -- reload schema cache + config on NOTIFY
AppState.runListener conf appState AppState.runListener conf appState
Admin.runAdmin conf appState (serverSettings conf) Admin.runAdmin appState (serverSettings conf)
let app = postgrest configLogLevel appState (AppState.connectionWorker appState) let app = postgrest configLogLevel appState (AppState.connectionWorker appState)
+60 -50
View File
@@ -9,7 +9,6 @@ module PostgREST.AppState
, destroy , destroy
, getConfig , getConfig
, getSchemaCache , getSchemaCache
, getIsListenerOn
, getMainThreadId , getMainThreadId
, getPgVersion , getPgVersion
, getRetryNextIn , getRetryNextIn
@@ -17,7 +16,6 @@ module PostgREST.AppState
, getJwtCache , getJwtCache
, getSocketREST , getSocketREST
, getSocketAdmin , getSocketAdmin
, getSchemaCacheLoaded
, init , init
, initSockets , initSockets
, initWithPool , initWithPool
@@ -28,6 +26,7 @@ module PostgREST.AppState
, connectionWorker , connectionWorker
, runListener , runListener
, getObserver , getObserver
, isLoaded
) where ) where
import qualified Data.Aeson as JSON import qualified Data.Aeson as JSON
@@ -90,14 +89,14 @@ data AppState = AppState
, statePgVersion :: IORef PgVersion , statePgVersion :: IORef PgVersion
-- | No schema cache at the start. Will be filled in by the connectionWorker -- | No schema cache at the start. Will be filled in by the connectionWorker
, stateSchemaCache :: IORef (Maybe SchemaCache) , stateSchemaCache :: IORef (Maybe SchemaCache)
-- | If schema cache is loaded -- | The schema cache status
, stateSchemaCacheLoaded :: IORef Bool , stateSCacheStatus :: IORef SchemaCacheStatus
-- | The connection status
, stateConnStatus :: IORef ConnectionStatus
-- | starts the connection worker with a debounce -- | starts the connection worker with a debounce
, debouncedConnectionWorker :: IO () , debouncedConnectionWorker :: IO ()
-- | Binary semaphore used to sync the listener(NOTIFY reload) with the connectionWorker. -- | Binary semaphore used to sync the listener(NOTIFY reload) with the connectionWorker.
, stateListener :: MVar () , stateListener :: MVar ()
-- | State of the LISTEN channel, used for the admin server checks
, stateIsListenerOn :: IORef Bool
-- | Config that can change at runtime -- | Config that can change at runtime
, stateConf :: IORef AppConfig , stateConf :: IORef AppConfig
-- | Time used for verifying JWT expiration -- | Time used for verifying JWT expiration
@@ -118,6 +117,20 @@ data AppState = AppState
, stateMetrics :: Metrics.MetricsState , stateMetrics :: Metrics.MetricsState
} }
-- | Schema cache status
data SchemaCacheStatus
= SCLoaded
| SCPending
| SCFatalFail
deriving Eq
-- | Current database connection status
data ConnectionStatus
= ConnEstablished
| ConnPending
| ConnFatalFail Text
deriving Eq
type AppSockets = (NS.Socket, Maybe NS.Socket) type AppSockets = (NS.Socket, Maybe NS.Socket)
@@ -138,10 +151,10 @@ initWithPool (sock, adminSock) pool conf loggerState metricsState observer = do
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
<*> newIORef Nothing <*> newIORef Nothing
<*> newIORef False <*> newIORef SCPending
<*> newIORef ConnPending
<*> pure (pure ()) <*> pure (pure ())
<*> newEmptyMVar <*> newEmptyMVar
<*> newIORef False
<*> newIORef conf <*> newIORef conf
<*> mkAutoUpdate defaultUpdateSettings { updateAction = getCurrentTime } <*> mkAutoUpdate defaultUpdateSettings { updateAction = getCurrentTime }
<*> myThreadId <*> myThreadId
@@ -286,29 +299,33 @@ waitListener = takeMVar . stateListener
signalListener :: AppState -> IO () signalListener :: AppState -> IO ()
signalListener appState = void $ tryPutMVar (stateListener appState) () signalListener appState = void $ tryPutMVar (stateListener appState) ()
getIsListenerOn :: AppState -> IO Bool isConnEstablished :: AppState -> IO Bool
getIsListenerOn = readIORef . stateIsListenerOn isConnEstablished x = do
conf <- getConfig x
if configDbChannelEnabled conf
then do -- if the listener is enabled, we can be sure the connection status is always up to date
st <- readIORef $ stateConnStatus x
return $ st == ConnEstablished
else -- otherwise the only way to check the connection is to make a query
isRight <$> usePool x (SQL.sql "SELECT 1")
putIsListenerOn :: AppState -> Bool -> IO () isLoaded :: AppState -> IO Bool
putIsListenerOn = atomicWriteIORef . stateIsListenerOn isLoaded x = do
scacheStatus <- readIORef $ stateSCacheStatus x
connEstablished <- isConnEstablished x
return $ scacheStatus == SCLoaded && connEstablished
getSchemaCacheLoaded :: AppState -> IO Bool putSCacheStatus :: AppState -> SchemaCacheStatus -> IO ()
getSchemaCacheLoaded = readIORef . stateSchemaCacheLoaded putSCacheStatus = atomicWriteIORef . stateSCacheStatus
putSchemaCacheLoaded :: AppState -> Bool -> IO () putConnStatus :: AppState -> ConnectionStatus -> IO ()
putSchemaCacheLoaded = atomicWriteIORef . stateSchemaCacheLoaded putConnStatus = atomicWriteIORef . stateConnStatus
getObserver :: AppState -> ObservationHandler getObserver :: AppState -> ObservationHandler
getObserver = stateObserver getObserver = stateObserver
-- | Schema cache status
data SCacheStatus
= SCLoaded
| SCOnRetry
| SCFatalFail
-- | Load the SchemaCache by using a connection from the pool. -- | Load the SchemaCache by using a connection from the pool.
loadSchemaCache :: AppState -> IO SCacheStatus loadSchemaCache :: AppState -> IO SchemaCacheStatus
loadSchemaCache appState@AppState{stateObserver=observer} = do loadSchemaCache appState@AppState{stateObserver=observer} = do
conf@AppConfig{..} <- getConfig appState conf@AppConfig{..} <- getConfig appState
(resultTime, result) <- (resultTime, result) <-
@@ -323,24 +340,17 @@ loadSchemaCache appState@AppState{stateObserver=observer} = do
Nothing -> do Nothing -> do
putSchemaCache appState Nothing putSchemaCache appState Nothing
observer $ SchemaCacheNormalErrorObs e observer $ SchemaCacheNormalErrorObs e
putSchemaCacheLoaded appState False putSCacheStatus appState SCPending
return SCOnRetry return SCPending
Right sCache -> do Right sCache -> do
putSchemaCache appState $ Just sCache putSchemaCache appState $ Just sCache
observer $ SchemaCacheQueriedObs resultTime observer $ SchemaCacheQueriedObs resultTime
(t, _) <- timeItT $ observer $ SchemaCacheSummaryObs $ showSummary sCache (t, _) <- timeItT $ observer $ SchemaCacheSummaryObs $ showSummary sCache
observer $ SchemaCacheLoadedObs t observer $ SchemaCacheLoadedObs t
putSchemaCacheLoaded appState True putSCacheStatus appState SCLoaded
return SCLoaded return SCLoaded
-- | Current database connection status data ConnectionStatus
data ConnectionStatus
= NotConnected
| Connected PgVersion
| FatalConnectionError Text
deriving (Eq)
-- | The purpose of this worker is to obtain a healthy connection to pg and an -- | The purpose of this worker is to obtain a healthy connection to pg and an
-- up-to-date schema cache(SchemaCache). This method is meant to be called -- up-to-date schema cache(SchemaCache). This method is meant to be called
-- multiple times by the same thread, but does nothing if the previous -- multiple times by the same thread, but does nothing if the previous
@@ -358,20 +368,19 @@ internalConnectionWorker appState@AppState{stateObserver=observer} = work
work = do work = do
AppConfig{..} <- getConfig appState AppConfig{..} <- getConfig appState
observer DBConnectAttemptObs observer DBConnectAttemptObs
connected <- establishConnection appState connStatus <- establishConnection appState
case connected of case connStatus of
FatalConnectionError reason -> ConnFatalFail reason ->
-- Fatal error when connecting -- Fatal error when connecting
observer (ExitFatalObs reason) >> killThread (getMainThreadId appState) observer (ExitFatalObs reason) >> killThread (getMainThreadId appState)
NotConnected -> ConnPending ->
-- Unreachable because establishConnection will keep trying to connect, unless disable-recovery is turned on
unless configDbPoolAutomaticRecovery unless configDbPoolAutomaticRecovery
$ observer ExitDBNoRecoveryObs >> killThread (getMainThreadId appState) $ observer ExitDBNoRecoveryObs >> killThread (getMainThreadId appState)
Connected actualPgVersion -> do ConnEstablished -> do
-- Procede with initialization -- Procede with initialization
putPgVersion appState actualPgVersion
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.
@@ -381,7 +390,7 @@ internalConnectionWorker appState@AppState{stateObserver=observer} = work
SCLoaded -> SCLoaded ->
-- do nothing and proceed if the load was successful -- do nothing and proceed if the load was successful
return () return ()
SCOnRetry -> SCPending ->
-- retry reloading the schema cache -- retry reloading the schema cache
work work
SCFatalFail -> SCFatalFail ->
@@ -415,23 +424,26 @@ establishConnection appState@AppState{stateObserver=observer} =
observer $ ConnectionPgVersionErrorObs e observer $ ConnectionPgVersionErrorObs e
case checkIsFatal e of case checkIsFatal e of
Just reason -> Just reason ->
return $ FatalConnectionError reason return $ ConnFatalFail reason
Nothing -> Nothing -> do
return NotConnected putConnStatus appState ConnPending
return ConnPending
Right version -> Right version ->
if version < minimumPgVersion then if version < minimumPgVersion then
return . FatalConnectionError $ return . ConnFatalFail $
"Cannot run in this PostgreSQL version, PostgREST needs at least " "Cannot run in this PostgreSQL version, PostgREST needs at least "
<> pgvName minimumPgVersion <> pgvName minimumPgVersion
else else do
return . Connected $ version 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
AppConfig{..} <- getConfig appState AppConfig{..} <- getConfig appState
let let
delay = fromMaybe 0 (rsPreviousDelay rs) `div` backoffMicroseconds delay = fromMaybe 0 (rsPreviousDelay rs) `div` backoffMicroseconds
itShould = NotConnected == isConnSucc && configDbPoolAutomaticRecovery itShould = ConnPending == isConnSucc && configDbPoolAutomaticRecovery
when itShould $ observer $ ConnectionRetryObs delay when itShould $ observer $ ConnectionRetryObs delay
when itShould $ putRetryNextIn appState delay when itShould $ putRetryNextIn appState delay
return itShould return itShould
@@ -503,7 +515,6 @@ listener appState@AppState{stateObserver=observer} conf@AppConfig{..} = do
case dbOrError of case dbOrError of
Right db -> do Right db -> do
observer $ DBListenerStart dbChannel observer $ DBListenerStart dbChannel
putIsListenerOn appState True
SQL.listen db $ SQL.toPgIdentifier dbChannel SQL.listen db $ SQL.toPgIdentifier dbChannel
SQL.waitForNotifications handleNotification db SQL.waitForNotifications handleNotification db
@@ -517,7 +528,6 @@ listener appState@AppState{stateObserver=observer} conf@AppConfig{..} = do
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
putIsListenerOn appState False
-- assume the pool connection was also lost, call the connection worker -- assume the pool connection was also lost, call the connection worker
connectionWorker appState connectionWorker appState
-- retry the listener -- retry the listener