refactor: do not open a TCP connection in liveness check

This change introduces a new way to perform liveness check - instead of trying to open a TCP connection to main server, we check if main socket is listening and if main server accept loop thread is alive.

Opening a TCP connection in liveness check was problematic because:
* it used available file descriptors which might have been a problem under load
* made liveness check unreliable when multiple PostgREST instances are available on the same port (eg. using SO_REUSEPORT)
This commit is contained in:
Michał Kłeczek
2026-07-10 12:45:59 -05:00
committed by Steve Chavez
parent 0bda2bcdff
commit 4fc47754c2
2 changed files with 27 additions and 24 deletions
+25 -23
View File
@@ -10,6 +10,7 @@ Some of its functionality includes:
- Content Negotiation - Content Negotiation
-} -}
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE ScopedTypeVariables #-}
@@ -19,8 +20,9 @@ module PostgREST.App
, run , run
) where ) where
import GHC.Conc (ThreadStatus (..), threadStatus)
import GHC.IO.Exception (IOErrorType (..)) import GHC.IO.Exception (IOErrorType (..))
import GHC.Weak
import System.IO.Error (ioeGetErrorType) import System.IO.Error (ioeGetErrorType)
import Control.Monad.Except (liftEither) import Control.Monad.Except (liftEither)
@@ -71,14 +73,14 @@ import qualified Data.Text as T
import qualified Network.HTTP.Types as HTTP import qualified Network.HTTP.Types as HTTP
import Network.HTTP.Types.Header (hVary) import Network.HTTP.Types.Header (hVary)
import qualified Network.Socket as NS import qualified Network.Socket as NS
import Network.Socket.ByteString (send)
import PostgREST.Unix (createAndBindDomainSocket) import PostgREST.Unix (createAndBindDomainSocket)
import System.Posix.Types (FileMode) import System.Posix.Types (FileMode)
import Protolude hiding (Handler) import Protolude hiding (Handler)
import System.Directory (doesPathExist)
run :: AppState -> IO () run :: AppState -> Weak ThreadId -> IO ()
run appState = do run appState mainThreadIdRef = do
conf <- AppState.getConfig appState conf <- AppState.getConfig appState
mainSocketRef <- newIORef Nothing mainSocketRef <- newIORef Nothing
@@ -92,7 +94,7 @@ run appState = do
ensureSocketClosed =<< readIORef mainSocketRef ensureSocketClosed =<< readIORef mainSocketRef
Unix.installSignalHandlers observer closeSockets (AppState.schemaCacheLoader appState) (AppState.readInDbConfig False appState) Unix.installSignalHandlers observer closeSockets (AppState.schemaCacheLoader appState) (AppState.readInDbConfig False appState)
Admin.runAdmin appState adminSocket (checkMainAppLive (readIORef mainSocketRef)) (serverSettings conf) Admin.runAdmin appState adminSocket (checkMainAppLive (readIORef mainSocketRef) mainThreadIdRef) (serverSettings conf)
Listener.runListener appState Listener.runListener appState
@@ -297,21 +299,21 @@ initAdminServerSocket AppConfig{..} =
configAdminServerUnixSocket configAdminServerUnixSocketMode configAdminServerUnixSocket configAdminServerUnixSocketMode
configAdminServerHost configAdminServerPort configAdminServerHost configAdminServerPort
checkMainAppLive :: IO (Maybe NS.Socket) -> IO Bool checkMainAppLive :: IO (Maybe NS.Socket) -> Weak ThreadId -> IO Bool
checkMainAppLive getMainSocket = checkMainAppLive getMainSocket mainThreadIdRef =
getMainSocket >>= maybe (pure False) (fmap isRight . reachMainApp) handle (\(_ :: IOException) -> pure False) $
checkMainThread <&&> checkSocket
-- Try to connect to the main app socket
-- Note that it doesn't even send a valid HTTP request, we just want to check that the main app is accepting connections
reachMainApp :: NS.Socket -> IO (Either IOException ())
reachMainApp appSock = do
sockAddr <- NS.getSocketName appSock
sock <- NS.socket (addrFamily sockAddr) NS.Stream NS.defaultProtocol
try $ do
NS.connect sock sockAddr
NS.withSocketsDo $ bracket (pure sock) NS.close sendEmpty
where where
sendEmpty sock = void $ send sock mempty checkSocket = getMainSocket >>=
addrFamily (NS.SockAddrInet _ _) = NS.AF_INET maybe (pure False)
addrFamily (NS.SockAddrInet6 {}) = NS.AF_INET6 (NS.getSocketName >=> \case
addrFamily (NS.SockAddrUnix _) = NS.AF_UNIX -- in case of unix socket, check if it still exists
NS.SockAddrUnix fp -> doesPathExist fp
_ -> pure True)
checkMainThread = deRefWeak mainThreadIdRef >>=
maybe (pure False)
(fmap isRunning . threadStatus)
isRunning = \case
ThreadRunning -> True
ThreadBlocked _ -> True
_ -> False
+2 -1
View File
@@ -43,6 +43,7 @@ runClientCommand conf CmdReady = Client.ready conf
runAppCommand :: AppConfig -> RunCommand -> IO () runAppCommand :: AppConfig -> RunCommand -> IO ()
runAppCommand conf@AppConfig{..} runCmd = do runAppCommand conf@AppConfig{..} runCmd = do
mainThreadId <- myThreadId mainThreadId <- myThreadId
mainThreadIdRef <- mkWeakThreadId mainThreadId
-- 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.
@@ -56,7 +57,7 @@ runAppCommand conf@AppConfig{..} runCmd = do
CmdDumpSchema -> do CmdDumpSchema -> do
when configDbConfig $ AppState.readInDbConfig True appState when configDbConfig $ AppState.readInDbConfig True appState
putStrLn =<< dumpSchema appState putStrLn =<< dumpSchema appState
CmdRun -> App.run appState) CmdRun -> App.run appState mainThreadIdRef)
-- | Dump SchemaCache schema to JSON -- | Dump SchemaCache schema to JSON
dumpSchema :: AppState -> IO LBS.ByteString dumpSchema :: AppState -> IO LBS.ByteString