Skip to content

Commit ca17636

Browse files
committed
fix: Start listening after schema cache load
This change ensures PostgREST starts listening on a server socket only after it loaded the schema cache and is ready to handle requests. It is no longer going to return 503 errors during startup until the schema cache is loaded.
1 parent fd3f937 commit ca17636

4 files changed

Lines changed: 50 additions & 79 deletions

File tree

src/PostgREST/Admin.hs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,20 @@ import qualified PostgREST.AppState as AppState
2222
import qualified Network.Socket as NS
2323
import Protolude
2424

25-
runAdmin :: AppState -> Maybe NS.Socket -> NS.Socket -> Warp.Settings -> IO ()
26-
runAdmin appState maybeAdminSocket socketREST settings = do
25+
runAdmin :: AppState -> Maybe NS.Socket -> IO (Maybe NS.Socket) -> Warp.Settings -> IO ()
26+
runAdmin appState maybeAdminSocket getSocketREST settings = do
2727
whenJust maybeAdminSocket $ \adminSocket -> do
2828
address <- resolveSocketToAddress adminSocket
2929
observer $ AdminStartObs address
3030
void . forkIO $ Warp.runSettingsSocket settings adminSocket adminApp
3131
where
32-
adminApp = admin appState socketREST
32+
adminApp = admin appState getSocketREST
3333
observer = AppState.getObserver appState
3434

3535
-- | PostgREST admin application
36-
admin :: AppState.AppState -> NS.Socket -> Wai.Application
37-
admin appState socketREST req respond = do
38-
isMainAppReachable <- isRight <$> reachMainApp socketREST
36+
admin :: AppState.AppState -> IO (Maybe NS.Socket) -> Wai.Application
37+
admin appState getSocketREST req respond = do
38+
isMainAppReachable <- getSocketREST >>= maybe (pure False) (fmap isRight . reachMainApp)
3939
isLoaded <- AppState.isLoaded appState
4040
isPending <- AppState.isPending appState
4141

@@ -44,8 +44,8 @@ admin appState socketREST req respond = do
4444
respond $ Wai.responseLBS (if isMainAppReachable then HTTP.status200 else HTTP.status500) [] mempty
4545
["ready"] ->
4646
let
47-
status | not isMainAppReachable = HTTP.status500
48-
| isPending = HTTP.status503
47+
status | isPending = HTTP.status503
48+
| not isMainAppReachable = HTTP.status500
4949
| isLoaded = HTTP.status200
5050
| otherwise = HTTP.status500
5151
in

src/PostgREST/App.hs

Lines changed: 32 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import System.IO.Error (ioeGetErrorType)
2424
import Control.Monad.Except (liftEither)
2525
import Control.Monad.Extra (whenJust)
2626
import Data.Either.Combinators (mapLeft, whenLeft)
27+
import Data.IORef (atomicWriteIORef, newIORef,
28+
readIORef)
2729
import Data.Maybe (fromJust)
2830
import Data.String (IsString (..))
2931
import Network.Wai.Handler.Warp (defaultSettings, setHost,
@@ -63,11 +65,10 @@ import PostgREST.Version (docsVersion, prettyVersion)
6365

6466
import qualified Data.ByteString.Char8 as BS
6567
import qualified Data.List as L
66-
import Data.Streaming.Network (bindPortTCP,
67-
bindRandomPortTCP)
68+
import Data.Streaming.Network (bindPortTCP)
6869
import qualified Data.Text as T
6970
import qualified Network.HTTP.Types as HTTP
70-
import qualified Network.HTTP.Types.Header as HTTP (hVary)
71+
import qualified Network.HTTP.Types.Header as HTTP
7172
import qualified Network.Socket as NS
7273
import PostgREST.Unix (createAndBindDomainSocket)
7374
import Protolude hiding (Handler)
@@ -78,22 +79,30 @@ run :: AppState -> IO ()
7879
run appState = do
7980
conf@AppConfig{..} <- AppState.getConfig appState
8081

81-
AppState.schemaCacheLoader appState -- Loads the initial SchemaCache
82-
(mainSocket, adminSocket) <- initSockets conf
82+
mainSocketRef <- newIORef Nothing
83+
adminSocket <- initAdminServerSocket conf
84+
8385
let closeSockets = do
8486
whenJust adminSocket NS.close
85-
NS.close mainSocket
87+
readIORef mainSocketRef >>= foldMap NS.close
8688
Unix.installSignalHandlers observer closeSockets (AppState.schemaCacheLoader appState) (AppState.readInDbConfig False appState)
8789

90+
Admin.runAdmin appState adminSocket (readIORef mainSocketRef) (serverSettings conf)
91+
8892
Listener.runListener appState
8993

90-
Admin.runAdmin appState adminSocket mainSocket (serverSettings conf)
94+
-- Kick off and wait for the initial SchemaCache load before creating the
95+
-- main API socket.
96+
AppState.schemaCacheLoader appState
97+
AppState.waitForSchemaCacheLoaded appState
98+
99+
mainSocket <- initServerSocket conf
100+
atomicWriteIORef mainSocketRef $ Just mainSocket
91101

92102
let app = postgrest configLogLevel appState (AppState.schemaCacheLoader appState)
93103

94-
do
95-
address <- resolveSocketToAddress mainSocket
96-
observer $ AppServerAddressObs address
104+
address <- resolveSocketToAddress mainSocket
105+
observer $ AppServerAddressObs address
97106

98107
Warp.runSettingsSocket (serverSettings conf & setOnException onWarpException) mainSocket app
99108
where
@@ -251,38 +260,16 @@ addRetryHint delay response = do
251260
isServiceUnavailable :: Wai.Response -> Bool
252261
isServiceUnavailable response = Wai.responseStatus response == HTTP.status503
253262

254-
type AppSockets = (NS.Socket, Maybe NS.Socket)
255-
256-
initSockets :: AppConfig -> IO AppSockets
257-
initSockets AppConfig{..} = do
258-
let
259-
cfg'usp = configServerUnixSocket
260-
cfg'uspm = configServerUnixSocketMode
261-
cfg'host = configServerHost
262-
cfg'port = configServerPort
263-
cfg'adminHost = configAdminServerHost
264-
cfg'adminPort = configAdminServerPort
265-
266-
sock <- case cfg'usp of
267-
-- I'm not using `streaming-commons`' bindPath function here because it's not defined for Windows,
268-
-- but we need to have runtime error if we try to use it in Windows, not compile time error
269-
Just path -> createAndBindDomainSocket path cfg'uspm
270-
Nothing -> do
271-
(_, sock) <-
272-
if cfg'port /= 0
273-
then do
274-
sock <- bindPortTCP cfg'port (fromString $ T.unpack cfg'host)
275-
pure (cfg'port, sock)
276-
else do
277-
-- explicitly bind to a random port, returning bound port number
278-
(num, sock) <- bindRandomPortTCP (fromString $ T.unpack cfg'host)
279-
pure (num, sock)
280-
pure sock
281-
282-
adminSock <- case cfg'adminPort of
283-
Just adminPort -> do
284-
adminSock <- bindPortTCP adminPort (fromString $ T.unpack cfg'adminHost)
285-
pure $ Just adminSock
286-
Nothing -> pure Nothing
287-
288-
pure (sock, adminSock)
263+
initServerSocket :: AppConfig -> IO NS.Socket
264+
initServerSocket AppConfig{..} = case configServerUnixSocket of
265+
-- I'm not using `streaming-commons`' bindPath function here because it's not defined for Windows,
266+
-- but we need to have runtime error if we try to use it in Windows, not compile time error
267+
Just path -> createAndBindDomainSocket path configServerUnixSocketMode
268+
Nothing -> bindPortTCP configServerPort (fromString $ T.unpack configServerHost)
269+
270+
initAdminServerSocket :: AppConfig -> IO (Maybe NS.Socket)
271+
initAdminServerSocket AppConfig{..} =
272+
traverse (`bindPortTCP` adminHost) configAdminServerPort
273+
where
274+
adminHost = fromString $ T.unpack configAdminServerHost
275+

src/PostgREST/AppState.hs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ module PostgREST.AppState
2727
, getObserver
2828
, isLoaded
2929
, isPending
30+
, waitForSchemaCacheLoaded
3031
) where
3132

3233
import qualified Data.ByteString.Char8 as BS
@@ -391,6 +392,9 @@ markSchemaCacheLoaded = void . (`tryPutMVar` ()) . getSCStatusMVar . stateSCache
391392
isSchemaCacheLoaded :: AppState -> IO Bool
392393
isSchemaCacheLoaded = fmap not . isEmptyMVar . getSCStatusMVar . stateSCacheStatus
393394

395+
waitForSchemaCacheLoaded :: AppState -> IO ()
396+
waitForSchemaCacheLoaded = void . readMVar . getSCStatusMVar . stateSCacheStatus
397+
394398
-- | Reads the in-db config and reads the config file again
395399
-- | We don't retry reading the in-db config after it fails immediately, because it could have user errors. We just report the error and continue.
396400
readInDbConfig :: Bool -> AppState -> IO ()

test/io/test_io.py

Lines changed: 6 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import subprocess
77
import time
88
import pytest
9+
import requests
910

1011
from config import CONFIGSDIR, FIXTURES, SECRET
1112
from util import Thread, jwtauthheader, parse_server_timings_header
@@ -1403,9 +1404,9 @@ def test_log_postgrest_host_and_port(host, defaultenv):
14031404
if is_unix:
14041405
re.match(r'API server listening on "/tmp/.*\.sock"', output[2])
14051406
elif is_ipv6(host):
1406-
assert f"API server listening on [{host}]:{port}" in output[2]
1407+
assert f"API server listening on [{host}]:{port}" in output[9]
14071408
else: # IPv4
1408-
assert f"API server listening on {host}:{port}" in output[2]
1409+
assert f"API server listening on {host}:{port}" in output[9]
14091410

14101411

14111412
def test_succeed_w_role_having_superuser_settings(defaultenv):
@@ -1753,27 +1754,6 @@ def test_pgrst_log_503_client_error_to_stderr(defaultenv):
17531754
assert any(log_message in line for line in output)
17541755

17551756

1756-
def test_log_error_when_empty_schema_cache_on_startup_to_stderr(defaultenv):
1757-
"Should log the 503 error message when there is an empty schema cache on startup"
1758-
1759-
env = {
1760-
**defaultenv,
1761-
"PGRST_INTERNAL_SCHEMA_CACHE_QUERY_SLEEP": "300",
1762-
}
1763-
1764-
with run(env=env, wait_for_readiness=False) as postgrest:
1765-
postgrest.wait_until_scache_starts_loading()
1766-
1767-
response = postgrest.session.get("/projects")
1768-
assert response.status_code == 503
1769-
1770-
output_start = postgrest.read_stdout(nlines=10)
1771-
1772-
log_err_message = '{"code":"PGRST002","details":null,"hint":null,"message":"Could not query the database for the schema cache. Retrying."}'
1773-
1774-
assert any(log_err_message in line for line in output_start)
1775-
1776-
17771757
def test_no_double_schema_cache_reload_on_empty_schema(defaultenv):
17781758
"Should only load the schema cache once on a 503 error when there's an empty schema cache on startup"
17791759

@@ -1785,8 +1765,8 @@ def test_no_double_schema_cache_reload_on_empty_schema(defaultenv):
17851765
with run(env=env, port=freeport(), wait_for_readiness=False) as postgrest:
17861766
postgrest.wait_until_scache_starts_loading()
17871767

1788-
response = postgrest.session.get("/projects")
1789-
assert response.status_code == 503
1768+
with pytest.raises(requests.ConnectionError):
1769+
postgrest.session.get("/projects")
17901770

17911771
# Should wait enough time to load the schema cache twice to guarantee that the test is valid
17921772
time.sleep(1)
@@ -1872,7 +1852,7 @@ def test_schema_cache_error_observation(defaultenv):
18721852
output = postgrest.read_stdout(nlines=9)
18731853
assert (
18741854
"Failed to load the schema cache using db-schemas=public and db-extra-search-path=x"
1875-
in output[7]
1855+
in output[6]
18761856
)
18771857

18781858

0 commit comments

Comments
 (0)