Skip to content

Commit 3b0a203

Browse files
committed
fix: Admin server dies silently
This is a workaround for Warp crashing with "thread blocked indefinitely in an STM transaction" when receiving eMFILE from accept. The fix is to provide Warp with a custom accept function that handles eMFILE by logging it and keeping accepting connections.
1 parent 568cab6 commit 3b0a203

4 files changed

Lines changed: 38 additions & 17 deletions

File tree

src/PostgREST/Admin.hs

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,48 +2,70 @@ module PostgREST.Admin
22
( runAdmin
33
) where
44

5+
import Control.Monad.Extra (whenJust)
56
import qualified Data.Aeson as JSON
7+
import Foreign.C.Error (Errno (..), eMFILE)
8+
import GHC.IO.Exception
69
import qualified Network.HTTP.Types.Status as HTTP
10+
import Network.Socket hiding (addrFamily)
11+
import Network.Socket.ByteString
712
import qualified Network.Wai as Wai
813
import qualified Network.Wai.Handler.Warp as Warp
914

10-
import Control.Monad.Extra (whenJust)
11-
import Network.Socket hiding (addrFamily)
12-
import Network.Socket.ByteString
13-
1415
import PostgREST.AppState (AppState, getConfig)
1516
import PostgREST.Config (AppConfig (..))
17+
import PostgREST.Debounce (makeDebouncer)
1618
import PostgREST.MediaType (MediaType (..), toContentType)
1719
import PostgREST.Metrics (metricsToText)
1820
import PostgREST.Network (resolveSocketToAddress)
1921
import PostgREST.Observation (Observation (..))
2022

2123
import qualified PostgREST.AppState as AppState
2224

23-
import qualified Network.Socket as NS
24-
import Protolude
25+
import Protolude
2526

26-
runAdmin :: AppState -> Maybe NS.Socket -> IO (Maybe NS.Socket) -> Warp.Settings -> IO ()
27+
runAdmin :: AppState -> Maybe Socket -> IO (Maybe Socket) -> Warp.Settings -> IO ()
2728
runAdmin appState maybeAdminSocket getSocketREST settings = do
2829
conf <- getConfig appState
2930
whenJust maybeAdminSocket $ \adminSocket -> do
3031
address <- resolveSocketToAddress adminSocket
32+
-- log EMFILE at most once every 10s
33+
logEMFILE <- makeDebouncer $ observer AdminServerAcceptEMFILEFailure *> threadDelay 10_000_000
34+
let
35+
safeAccept sock =
36+
-- TODO use catchNoPropagate and rethrowIO once
37+
-- we stop supporting GHC versions < 9.12.1
38+
accept sock `catch`
39+
\e ->
40+
-- Keep accepting on eMFILE
41+
if isEMFILE e then do
42+
logEMFILE
43+
-- Backoff for 1ms so that we don't enter busy accept loop
44+
-- this should let the system recover fd's once the pressure is gone
45+
threadDelay 1_000
46+
safeAccept sock
47+
else
48+
throwIO e
49+
adminServerSettings config addr =
50+
settings
51+
& Warp.setAccept safeAccept
52+
& Warp.setBeforeMainLoop (observer $ AdminStartObs addr)
53+
& maybe identity Warp.setPort (configAdminServerPort config)
54+
3155
void . forkIO $ handle (onError adminSocket) $
3256
Warp.runSettingsSocket (adminServerSettings conf address) adminSocket adminApp
3357
where
3458
adminApp = admin appState getSocketREST
3559
observer = AppState.getObserver appState
36-
adminServerSettings config addr=
37-
settings
38-
& Warp.setBeforeMainLoop (observer $ AdminStartObs addr)
39-
& maybe identity Warp.setPort (configAdminServerPort config)
60+
61+
isEMFILE = (Just eMFILE ==) . fmap Errno . ioe_errno
4062

4163
onError adminSock ex = do
4264
observer $ AdminServerCrashedObs ex
43-
NS.close adminSock -- we close the socket so request doesn't hang
65+
close adminSock -- we close the socket so request doesn't hang
4466

4567
-- | PostgREST admin application
46-
admin :: AppState.AppState -> IO (Maybe NS.Socket) -> Wai.Application
68+
admin :: AppState.AppState -> IO (Maybe Socket) -> Wai.Application
4769
admin appState getSocketREST req respond = do
4870
isMainAppReachable <- getSocketREST >>= maybe (pure False) (fmap isRight . reachMainApp)
4971
isLoaded <- AppState.isLoaded appState

src/PostgREST/Logger.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ observationMessages = \case
134134
pure $ "Admin server listening on " <> address
135135
AdminServerCrashedObs ex ->
136136
pure $ "FAILURE: Admin server crashed unexpectedly: " <> (showOnSingleLine '\t' . show) ex
137+
AdminServerAcceptEMFILEFailure ->
138+
pure "Admin server failed to accept connection due to file descriptor exhaustion (EMFILE)"
137139
AppStartObs ver ->
138140
pure $ "Starting PostgREST " <> T.decodeUtf8 ver <> "..."
139141
AppServerAddressObs address ->

src/PostgREST/Observation.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import Protolude hiding (toList)
2626
data Observation
2727
= AdminStartObs Text
2828
| AdminServerCrashedObs SomeException
29+
| AdminServerAcceptEMFILEFailure
2930
| AppStartObs ByteString
3031
| AppServerAddressObs Text
3132
| ExitUnsupportedPgVersion PgVersion PgVersion

test/io/test_io.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -846,10 +846,6 @@ def test_admin_live_dependent_on_main_app(defaultenv):
846846
assert response.status_code == 500
847847

848848

849-
@pytest.mark.xfail(
850-
reason="Admin server crashes when the first admin accept hits EMFILE",
851-
strict=True,
852-
)
853849
def test_admin_server_does_not_crash_when_file_limit_is_reached(defaultenv):
854850
"Admin server should keep running when accept reaches the open file limit."
855851

0 commit comments

Comments
 (0)