fix: incorrect /ready response on slow schema load

This commit is contained in:
steve-chavez
2024-03-15 11:03:01 -05:00
committed by Steve Chavez
parent 4b289b1c97
commit 11d8da046c
5 changed files with 38 additions and 5 deletions
+1
View File
@@ -23,6 +23,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- #3160, Fix using select= query parameter for custom media type handlers - @wolfgangwalther - #3160, Fix using select= query parameter for custom media type handlers - @wolfgangwalther
- #3237, Dump media handlers and timezones with --dump-schema - @wolfgangwalther - #3237, Dump media handlers and timezones with --dump-schema - @wolfgangwalther
- #3323, #3324, Don't hide error on LISTEN channel failure - @steve-chavez - #3323, #3324, Don't hide error on LISTEN channel failure - @steve-chavez
- #3330, Incorrect admin server `/ready` response on slow schema cache loads - @steve-chavez
### Deprecated ### Deprecated
+1 -1
View File
@@ -38,7 +38,7 @@ runAdmin conf@AppConfig{configAdminServerPort} appState settings observer =
admin :: AppState.AppState -> AppConfig -> (Observation -> IO ()) -> Wai.Application admin :: AppState.AppState -> AppConfig -> (Observation -> IO ()) -> Wai.Application
admin appState appConfig observer req respond = do admin appState appConfig observer req respond = do
isMainAppReachable <- isRight <$> reachMainApp (AppState.getSocketREST appState) isMainAppReachable <- isRight <$> reachMainApp (AppState.getSocketREST appState)
isSchemaCacheLoaded <- isJust <$> AppState.getSchemaCache appState isSchemaCacheLoaded <- AppState.getSchemaCacheLoaded appState
isConnectionUp <- isConnectionUp <-
if configDbChannelEnabled appConfig if configDbChannelEnabled appConfig
then AppState.getIsListenerOn appState then AppState.getIsListenerOn appState
+12
View File
@@ -16,6 +16,7 @@ module PostgREST.AppState
, getJwtCache , getJwtCache
, getSocketREST , getSocketREST
, getSocketAdmin , getSocketAdmin
, getSchemaCacheLoaded
, init , init
, initSockets , initSockets
, initWithPool , initWithPool
@@ -85,6 +86,8 @@ 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
, stateSchemaCacheLoaded :: IORef Bool
-- | 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.
@@ -123,6 +126,7 @@ initWithPool (sock, adminSock) pool conf 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
<*> pure (pure ()) <*> pure (pure ())
<*> newEmptyMVar <*> newEmptyMVar
<*> newIORef False <*> newIORef False
@@ -283,6 +287,12 @@ getIsListenerOn = readIORef . stateIsListenerOn
putIsListenerOn :: AppState -> Bool -> IO () putIsListenerOn :: AppState -> Bool -> IO ()
putIsListenerOn = atomicWriteIORef . stateIsListenerOn putIsListenerOn = atomicWriteIORef . stateIsListenerOn
getSchemaCacheLoaded :: AppState -> IO Bool
getSchemaCacheLoaded = readIORef . stateSchemaCacheLoaded
putSchemaCacheLoaded :: AppState -> Bool -> IO ()
putSchemaCacheLoaded = atomicWriteIORef . stateSchemaCacheLoaded
-- | Schema cache status -- | Schema cache status
data SCacheStatus data SCacheStatus
= SCLoaded = SCLoaded
@@ -305,6 +315,7 @@ loadSchemaCache appState observer = do
Nothing -> do Nothing -> do
putSchemaCache appState Nothing putSchemaCache appState Nothing
observer $ SchemaCacheNormalErrorObs e observer $ SchemaCacheNormalErrorObs e
putSchemaCacheLoaded appState False
return SCOnRetry return SCOnRetry
Right sCache -> do Right sCache -> do
@@ -312,6 +323,7 @@ loadSchemaCache appState observer = do
observer $ SchemaCacheQueriedObs resultTime observer $ SchemaCacheQueriedObs resultTime
(t, _) <- timeItT $ observer $ SchemaCacheSummaryObs sCache (t, _) <- timeItT $ observer $ SchemaCacheSummaryObs sCache
observer $ SchemaCacheLoadedObs t observer $ SchemaCacheLoadedObs t
putSchemaCacheLoaded appState True
return SCLoaded return SCLoaded
-- | Current database connection status data ConnectionStatus -- | Current database connection status data ConnectionStatus
+4 -4
View File
@@ -78,6 +78,7 @@ def run(
port=None, port=None,
host=None, host=None,
wait_for_readiness=True, wait_for_readiness=True,
wait_max_seconds=1,
no_pool_connection_available=False, no_pool_connection_available=False,
no_startup_stdout=True, no_startup_stdout=True,
): ):
@@ -118,7 +119,7 @@ def run(
process.stdin.close() process.stdin.close()
if wait_for_readiness: if wait_for_readiness:
wait_until_ready(adminurl + "/ready") wait_until_ready(adminurl + "/ready", wait_max_seconds)
if no_startup_stdout: if no_startup_stdout:
process.stdout.read() process.stdout.read()
@@ -176,12 +177,11 @@ def wait_until_exit(postgrest):
raise PostgrestTimedOut() raise PostgrestTimedOut()
def wait_until_ready(url): def wait_until_ready(url, max_seconds):
"Wait for the given HTTP endpoint to return a status of 200." "Wait for the given HTTP endpoint to return a status of 200."
session = requests_unixsocket.Session() session = requests_unixsocket.Session()
response = None for _ in range(max_seconds * 10):
for _ in range(10):
try: try:
response = session.get(url, timeout=1) response = session.get(url, timeout=1)
if response.status_code == 200: if response.status_code == 200:
+20
View File
@@ -7,6 +7,26 @@ from util import *
from postgrest import * from postgrest import *
def test_first_request_succeeds(defaultenv):
"the first request suceeds fast since the admin server readiness was checked before"
env = {
**defaultenv,
"PGRST_DB_SCHEMAS": "apflora",
"PGRST_DB_POOL": "2",
"PGRST_DB_ANON_ROLE": "postgrest_test_anonymous",
"PGRST_SERVER_TIMING_ENABLED": "true",
}
with run(env=env, wait_max_seconds=20) as postgrest:
response = postgrest.session.get("/tpopmassn?select=*,tpop(*)")
assert response.status_code == 200
server_timings = parse_server_timings_header(response.headers["Server-Timing"])
plan_dur = server_timings["plan"]
assert plan_dur < 2.0
def test_requests_wait_for_schema_cache_to_be_loaded(defaultenv): def test_requests_wait_for_schema_cache_to_be_loaded(defaultenv):
"requests that use the schema cache (e.g. resource embedding) wait for schema cache to be loaded" "requests that use the schema cache (e.g. resource embedding) wait for schema cache to be loaded"